Skip to content

Commit

Permalink
feat(rds): add support for joining instance to domain (#9943)
Browse files Browse the repository at this point in the history
Added new properties to be able to join instance to a domain.

closes #9869


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
ctaylor-osv committed Sep 1, 2020
1 parent 5bed08a commit f2d77d1
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 1 deletion.
31 changes: 31 additions & 0 deletions packages/@aws-cdk/aws-rds/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`):
Expand Down
14 changes: 14 additions & 0 deletions packages/@aws-cdk/aws-rds/lib/instance-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -184,6 +191,13 @@ class MariaDbInstanceEngine extends InstanceEngineBase {
: undefined,
});
}

public bindToInstance(scope: core.Construct, options: InstanceEngineBindOptions): InstanceEngineConfig {
if (options.domain) {
throw new Error(`domain property cannot be configured for ${this.engineType}`);
}
return super.bindToInstance(scope, options);
}
}

/**
Expand Down
30 changes: 30 additions & 0 deletions packages/@aws-cdk/aws-rds/lib/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,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;
}

/**
Expand All @@ -513,6 +528,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) {
Expand Down Expand Up @@ -555,6 +573,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,
Expand Down Expand Up @@ -587,6 +615,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,
};
}

Expand Down
123 changes: 122 additions & 1 deletion packages/@aws-cdk/aws-rds/test/test.instance.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -757,4 +757,125 @@ 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();
},

'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();
},
};

0 comments on commit f2d77d1

Please sign in to comment.