From bfb0c6e5dd75abadef423b828c45c6b09ac5f35f Mon Sep 17 00:00:00 2001 From: ctaylor-osv Date: Mon, 24 Aug 2020 18:01:01 +0000 Subject: [PATCH 1/5] feat(rds): add support for joining instance to domain Added new properties to be able to join instance to a domain. closes #9869 --- packages/@aws-cdk/aws-rds/lib/instance.ts | 34 ++++++ .../@aws-cdk/aws-rds/test/test.instance.ts | 103 +++++++++++++++++- 2 files changed, 136 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index dd8c100ca925b..1cfb00a52e1bb 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -492,6 +492,21 @@ export interface DatabaseInstanceNewProps { * @default - No autoscaling of RDS instance */ readonly maxAllocatedStorage?: number; + + /** + * The Active Directory directory ID to create the DB instance in. + * + * @default - Do not join domain + */ + readonly domain?: string; + + /** + * The IAM role to be used when making API calls to the Directory Service. The role needs the AWS-managed policy + * AmazonRDSDirectoryServiceAccess or equivalent. + * + * @default - The role will be created for you if {@link DatabaseInstanceNewProps#domain} is specified + */ + readonly domainRole?: iam.IRole; } /** @@ -514,6 +529,9 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData private readonly cloudwatchLogsRetention?: logs.RetentionDays; private readonly cloudwatchLogsRetentionRole?: iam.IRole; + private readonly domainId?: string; + private readonly domainRole?: iam.IRole; + protected enableIamAuthentication?: boolean; constructor(scope: Construct, id: string, props: DatabaseInstanceNewProps) { @@ -556,6 +574,16 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData this.cloudwatchLogsRetentionRole = props.cloudwatchLogsRetentionRole; this.enableIamAuthentication = props.iamAuthentication; + if (props.domain) { + this.domainId = props.domain; + this.domainRole = props.domainRole || new iam.Role(this, 'RDSDirectoryServiceRole', { + assumedBy: new iam.ServicePrincipal('rds.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonRDSDirectoryServiceAccess'), + ], + }); + } + this.newCfnProps = { autoMinorVersionUpgrade: props.autoMinorVersionUpgrade, availabilityZone: props.multiAz ? undefined : props.availabilityZone, @@ -588,6 +616,8 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData storageType, vpcSecurityGroups: securityGroups.map(s => s.securityGroupId), maxAllocatedStorage: props.maxAllocatedStorage, + domain: this.domainId, + domainIamRoleName: this.domainRole?.roleName, }; } @@ -698,6 +728,10 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa this.singleUserRotationApplication = props.engine.singleUserRotationApplication; this.multiUserRotationApplication = props.engine.multiUserRotationApplication; + if (props.domain && !props.engine.engineType.match(/^(mysql|postgres|oracle|sqlserver)/)) { + throw new Error('Cannot specify `domain` unless engine is MySQL, Oracle, PostgreSQL, or SQL Server.'); + } + props.engine.bindToInstance(this, props); this.instanceType = props.instanceType ?? ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE); diff --git a/packages/@aws-cdk/aws-rds/test/test.instance.ts b/packages/@aws-cdk/aws-rds/test/test.instance.ts index 3383f4f93cd55..fc62f68c0443d 100644 --- a/packages/@aws-cdk/aws-rds/test/test.instance.ts +++ b/packages/@aws-cdk/aws-rds/test/test.instance.ts @@ -1,4 +1,4 @@ -import { ABSENT, countResources, expect, haveResource, ResourcePart, haveResourceLike } from '@aws-cdk/assert'; +import { ABSENT, countResources, expect, haveResource, ResourcePart, haveResourceLike, anything } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as targets from '@aws-cdk/aws-events-targets'; import { ManagedPolicy, Role, ServicePrincipal, AccountPrincipal } from '@aws-cdk/aws-iam'; @@ -757,4 +757,105 @@ export = { test.done(); }, + 'domain - sets domain property'(test: Test) { + const domain = 'd-90670a8d36'; + + // WHEN + new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.sqlServerWeb({ version: rds.SqlServerEngineVersion.VER_14_00_3192_2_V1 }), + vpc, + masterUsername: 'admin', + domain: domain, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + Domain: domain, + })); + + test.done(); + }, + + 'domain - uses role if provided'(test: Test) { + const domain = 'd-90670a8d36'; + + // WHEN + const role = new Role(stack, 'DomainRole', { assumedBy: new ServicePrincipal('rds.amazonaws.com') }); + new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.sqlServerWeb({ version: rds.SqlServerEngineVersion.VER_14_00_3192_2_V1 }), + vpc, + masterUsername: 'admin', + domain: domain, + domainRole: role, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + Domain: domain, + DomainIAMRoleName: stack.resolve(role.roleName), + })); + + test.done(); + }, + + 'domain - creates role if not provided'(test: Test) { + const domain = 'd-90670a8d36'; + + // WHEN + new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.sqlServerWeb({ version: rds.SqlServerEngineVersion.VER_14_00_3192_2_V1 }), + vpc, + masterUsername: 'admin', + domain: domain, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + Domain: domain, + DomainIAMRoleName: anything(), + })); + + expect(stack).to(haveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'rds.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::aws:policy/service-role/AmazonRDSDirectoryServiceAccess', + ], + ], + }, + ], + })); + + test.done(); + }, + + 'domain - throws if incompatible engine type'(test: Test) { + const domain = 'd-90670a8d36'; + test.throws(() => new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.mariaDb({ version: rds.MariaDbEngineVersion.VER_10_4_8 }), + vpc, + masterUsername: 'admin', + domain: domain, + }), 'Cannot specify `domain` unless engine is MySQL, Oracle, PostgreSQL, or SQL Server.'); + + test.done(); + }, }; From 2c449cd2405562f7d6af51c66098b18d74028e8f Mon Sep 17 00:00:00 2001 From: ctaylor-osv Date: Mon, 24 Aug 2020 19:19:56 +0000 Subject: [PATCH 2/5] Updated readme with examples of enabling domain support --- packages/@aws-cdk/aws-rds/README.md | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 829e7237f33e6..d517a001b8bb6 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -228,6 +228,37 @@ instance.grantConnect(role); // Grant the role connection access to the DB. **Note**: In addition to the setup above, a database user will need to be created to support IAM auth. See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.DBAccounts.html for setup instructions. +### Kerberos Authentication + +You can also authenticate using Kerberos to a database instance using AWS Managed Microsoft AD for authentication; +See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/kerberos-authentication.html for more information +and a list of supported versions and limitations. + +The following example shows enabling domain support for a database instance and creating an IAM role to access +Directory Services. + +```ts +const role = new iam.Role(stack, 'RDSDirectoryServicesRole', { + assumedBy: new iam.ServicePrincipal('rds.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonRDSDirectoryServiceAccess'), + ], +}); +const instance = new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), + masterUsername: 'admin', + vpc, + domain: 'd-????????', // The ID of the domain for the instance to join. + domainRole: role, // Optional - will be create automatically if not provided. +}); +``` + +**Note**: In addition to the setup above, you need to make sure that the database instance has network connectivity +to the domain controllers. This includes enabling cross-VPC traffic if in a different VPC and setting up the +appropriate security groups/network ACL to allow traffic between the database instance and domain controllers. +Once configured, see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/kerberos-authentication.html for details +on configuring users for each available database engine. + ### Metrics Database instances expose metrics (`cloudwatch.Metric`): From d14900a92ecae73da4759c8aa8dd6795ff9d3808 Mon Sep 17 00:00:00 2001 From: ctaylor-osv Date: Tue, 1 Sep 2020 12:39:28 +0000 Subject: [PATCH 3/5] Moved domain property check to instance engine --- .../@aws-cdk/aws-rds/lib/instance-engine.ts | 16 +++++++++ packages/@aws-cdk/aws-rds/lib/instance.ts | 4 --- .../@aws-cdk/aws-rds/test/test.instance.ts | 36 ++++++++++++++----- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts index 35ccb3fbea7f2..4d849eb606d38 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts @@ -7,6 +7,13 @@ import { EngineVersion } from './engine-version'; * The options passed to {@link IInstanceEngine.bind}. */ export interface InstanceEngineBindOptions { + /** + * The Active Directory directory ID to create the DB instance in. + * + * @default - none (it's an optional field) + */ + readonly domain?: string; + /** * The timezone of the database, set by the customer. * @@ -184,6 +191,15 @@ class MariaDbInstanceEngine extends InstanceEngineBase { : undefined, }); } + + public bindToInstance(_scope: core.Construct, _options: InstanceEngineBindOptions): InstanceEngineConfig { + super.bindToInstance(_scope, _options); + if (_options.domain) { + throw new Error(`domain property cannot be configured for ${this.engineType}`); + } + return { + }; + } } /** diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index 1cfb00a52e1bb..eeac9179d0ca7 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -728,10 +728,6 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa this.singleUserRotationApplication = props.engine.singleUserRotationApplication; this.multiUserRotationApplication = props.engine.multiUserRotationApplication; - if (props.domain && !props.engine.engineType.match(/^(mysql|postgres|oracle|sqlserver)/)) { - throw new Error('Cannot specify `domain` unless engine is MySQL, Oracle, PostgreSQL, or SQL Server.'); - } - props.engine.bindToInstance(this, props); this.instanceType = props.instanceType ?? ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE); diff --git a/packages/@aws-cdk/aws-rds/test/test.instance.ts b/packages/@aws-cdk/aws-rds/test/test.instance.ts index fc62f68c0443d..6a92c763621ed 100644 --- a/packages/@aws-cdk/aws-rds/test/test.instance.ts +++ b/packages/@aws-cdk/aws-rds/test/test.instance.ts @@ -847,14 +847,34 @@ export = { test.done(); }, - 'domain - throws if incompatible engine type'(test: Test) { - const domain = 'd-90670a8d36'; - test.throws(() => new rds.DatabaseInstance(stack, 'Instance', { - engine: rds.DatabaseInstanceEngine.mariaDb({ version: rds.MariaDbEngineVersion.VER_10_4_8 }), - vpc, - masterUsername: 'admin', - domain: domain, - }), 'Cannot specify `domain` unless engine is MySQL, Oracle, PostgreSQL, or SQL Server.'); + 'throws when domain is set for mariadb database engine'(test: Test) { + const domainSupportedEngines = [rds.DatabaseInstanceEngine.SQL_SERVER_EE, rds.DatabaseInstanceEngine.SQL_SERVER_EX, + rds.DatabaseInstanceEngine.SQL_SERVER_SE, rds.DatabaseInstanceEngine.SQL_SERVER_WEB, rds.DatabaseInstanceEngine.MYSQL, + rds.DatabaseInstanceEngine.POSTGRES, rds.DatabaseInstanceEngine.ORACLE_EE]; + const domainUnsupportedEngines = [rds.DatabaseInstanceEngine.MARIADB]; + + // THEN + domainSupportedEngines.forEach((engine) => { + test.ok(new rds.DatabaseInstance(stack, `${engine.engineType}-db`, { + engine, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.SMALL), + masterUsername: 'master', + domain: 'd-90670a8d36', + vpc, + })); + }); + + domainUnsupportedEngines.forEach((engine) => { + const expectedError = new RegExp(`domain property cannot be configured for ${engine.engineType}`); + + test.throws(() => new rds.DatabaseInstance(stack, `${engine.engineType}-db`, { + engine, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.SMALL), + masterUsername: 'master', + domain: 'd-90670a8d36', + vpc, + }), expectedError); + }); test.done(); }, From 90793f1504ddd6d0501e2332639f339a04fca287 Mon Sep 17 00:00:00 2001 From: ctaylor-osv Date: Tue, 1 Sep 2020 13:13:52 +0000 Subject: [PATCH 4/5] Added capture of return value of super call --- packages/@aws-cdk/aws-rds/lib/instance-engine.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts index 4d849eb606d38..fa1be09ec5cd0 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts @@ -193,12 +193,11 @@ class MariaDbInstanceEngine extends InstanceEngineBase { } public bindToInstance(_scope: core.Construct, _options: InstanceEngineBindOptions): InstanceEngineConfig { - super.bindToInstance(_scope, _options); + const config = super.bindToInstance(_scope, _options); if (_options.domain) { throw new Error(`domain property cannot be configured for ${this.engineType}`); } - return { - }; + return { ...config }; } } From a1ca20005f28a0b0ad102b69c5f4bda71e2480b8 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Tue, 1 Sep 2020 11:51:22 -0700 Subject: [PATCH 5/5] Get rid of underscores in MariaDbInstanceEngine.bindToInstance() parameters --- packages/@aws-cdk/aws-rds/lib/instance-engine.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts index fa1be09ec5cd0..df715c9e0387a 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts @@ -192,12 +192,11 @@ class MariaDbInstanceEngine extends InstanceEngineBase { }); } - public bindToInstance(_scope: core.Construct, _options: InstanceEngineBindOptions): InstanceEngineConfig { - const config = super.bindToInstance(_scope, _options); - if (_options.domain) { + public bindToInstance(scope: core.Construct, options: InstanceEngineBindOptions): InstanceEngineConfig { + if (options.domain) { throw new Error(`domain property cannot be configured for ${this.engineType}`); } - return { ...config }; + return super.bindToInstance(scope, options); } }