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(rds): add support for joining instance to domain #9943

Merged
merged 7 commits into from
Sep 1, 2020
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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, was trying to model off of the bindToInstance method from the SqlServerInstanceEngineBase class which has the parameters with underscores. Is that because the method in that class doesn't use them?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly 🙂. No worries!

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