Skip to content

Commit 7945fa6

Browse files
authored
Merge pull request #23554 from sean-beath/feat(redshift)/support-default-role-for-redshift-clusters
feat(redshift): support default role for redshift clusters
2 parents 39f8a30 + e5d6589 commit 7945fa6

13 files changed

+3422
-0
lines changed

packages/@aws-cdk/aws-redshift/README.md

+45
Original file line numberDiff line numberDiff line change
@@ -386,3 +386,48 @@ new Cluster(stack, 'Redshift', {
386386
```
387387

388388
If enhanced VPC routing is not enabled, Amazon Redshift routes traffic through the internet, including traffic to other services within the AWS network.
389+
390+
## Default IAM role
391+
392+
Some Amazon Redshift features require Amazon Redshift to access other AWS services on your behalf. For your Amazon Redshift clusters to act on your behalf, you supply security credentials to your clusters. The preferred method to supply security credentials is to specify an AWS Identity and Access Management (IAM) role.
393+
394+
When you create an IAM role and set it as the default for the cluster using console, you don't have to provide the IAM role's Amazon Resource Name (ARN) to perform authentication and authorization.
395+
396+
```ts
397+
declare const vpc: ec2.Vpc;
398+
399+
const defaultRole = new iam.Role(this, 'DefaultRole', {
400+
assumedBy: new iam.ServicePrincipal('redshift.amazonaws.com'),
401+
},
402+
);
403+
404+
new Cluster(stack, 'Redshift', {
405+
masterUser: {
406+
masterUsername: 'admin',
407+
},
408+
vpc,
409+
roles: [defaultRole],
410+
defaultRole: defaultRole,
411+
});
412+
```
413+
414+
A default role can also be added to a cluster using the `addDefaultIamRole` method.
415+
416+
```ts
417+
declare const vpc: ec2.Vpc;
418+
419+
const defaultRole = new iam.Role(this, 'DefaultRole', {
420+
assumedBy: new iam.ServicePrincipal('redshift.amazonaws.com'),
421+
},
422+
);
423+
424+
const redshiftCluster = new Cluster(stack, 'Redshift', {
425+
masterUser: {
426+
masterUsername: 'admin',
427+
},
428+
vpc,
429+
roles: [defaultRole],
430+
});
431+
432+
redshiftCluster.addDefaultIamRole(defaultRole);
433+
```

packages/@aws-cdk/aws-redshift/lib/cluster.ts

+71
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as kms from '@aws-cdk/aws-kms';
44
import * as s3 from '@aws-cdk/aws-s3';
55
import * as secretsmanager from '@aws-cdk/aws-secretsmanager';
66
import { Duration, IResource, RemovalPolicy, Resource, SecretValue, Token } from '@aws-cdk/core';
7+
import { AwsCustomResource, PhysicalResourceId, AwsCustomResourcePolicy } from '@aws-cdk/custom-resources';
78
import { Construct } from 'constructs';
89
import { DatabaseSecret } from './database-secret';
910
import { Endpoint } from './endpoint';
@@ -304,6 +305,14 @@ export interface ClusterProps {
304305
*/
305306
readonly roles?: iam.IRole[];
306307

308+
/**
309+
* A single AWS Identity and Access Management (IAM) role to be used as the default role for the cluster.
310+
* The default role must be included in the roles list.
311+
*
312+
* @default - No default role is specified for the cluster.
313+
*/
314+
readonly defaultRole?: iam.IRole;
315+
307316
/**
308317
* Name of a database which is automatically created inside the cluster
309318
*
@@ -575,6 +584,15 @@ export class Cluster extends ClusterBase {
575584

576585
const defaultPort = ec2.Port.tcp(this.clusterEndpoint.port);
577586
this.connections = new ec2.Connections({ securityGroups, defaultPort });
587+
588+
// Add default role if specified and also available in the roles list
589+
if (props.defaultRole) {
590+
if (props.roles?.some(x => x === props.defaultRole)) {
591+
this.addDefaultIamRole(props.defaultRole);
592+
} else {
593+
throw new Error('Default role must be included in role list.');
594+
}
595+
}
578596
}
579597

580598
/**
@@ -662,4 +680,57 @@ export class Cluster extends ClusterBase {
662680
throw new Error('Cannot add a parameter to an imported parameter group.');
663681
}
664682
}
683+
684+
/**
685+
* Adds default IAM role to cluster. The default IAM role must be already associated to the cluster to be added as the default role.
686+
*
687+
* @param defaultIamRole the IAM role to be set as the default role
688+
*/
689+
public addDefaultIamRole(defaultIamRole: iam.IRole): void {
690+
// Get list of IAM roles attached to cluster
691+
const clusterRoleList = this.cluster.iamRoles ?? [];
692+
693+
// Check to see if default role is included in list of cluster IAM roles
694+
var roleAlreadyOnCluster = false;
695+
for (var i = 0; i < clusterRoleList.length; i++) {
696+
if (clusterRoleList[i] == defaultIamRole.roleArn) {
697+
roleAlreadyOnCluster = true;
698+
break;
699+
}
700+
}
701+
if (!roleAlreadyOnCluster) {
702+
throw new Error('Default role must be associated to the Redshift cluster to be set as the default role.');
703+
}
704+
705+
// On UPDATE or CREATE define the default IAM role. On DELETE, remove the default IAM role
706+
const defaultRoleCustomResource = new AwsCustomResource(this, 'default-role', {
707+
onUpdate: {
708+
service: 'Redshift',
709+
action: 'modifyClusterIamRoles',
710+
parameters: {
711+
ClusterIdentifier: this.cluster.ref,
712+
DefaultIamRoleArn: defaultIamRole.roleArn,
713+
},
714+
physicalResourceId: PhysicalResourceId.of(
715+
`${defaultIamRole.roleArn}-${this.cluster.ref}`,
716+
),
717+
},
718+
onDelete: {
719+
service: 'Redshift',
720+
action: 'modifyClusterIamRoles',
721+
parameters: {
722+
ClusterIdentifier: this.cluster.ref,
723+
DefaultIamRoleArn: '',
724+
},
725+
physicalResourceId: PhysicalResourceId.of(
726+
`${defaultIamRole.roleArn}-${this.cluster.ref}`,
727+
),
728+
},
729+
policy: AwsCustomResourcePolicy.fromSdkCalls({
730+
resources: AwsCustomResourcePolicy.ANY_RESOURCE,
731+
}),
732+
});
733+
734+
defaultIamRole.grantPassRole(defaultRoleCustomResource.grantPrincipal);
735+
}
665736
}

packages/@aws-cdk/aws-redshift/test/cluster.test.ts

+35
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Match, Template } from '@aws-cdk/assertions';
22
import * as ec2 from '@aws-cdk/aws-ec2';
3+
import * as iam from '@aws-cdk/aws-iam';
34
import * as kms from '@aws-cdk/aws-kms';
45
import * as s3 from '@aws-cdk/aws-s3';
56
import * as cdk from '@aws-cdk/core';
@@ -613,6 +614,40 @@ test('elastic ip address', () => {
613614
});
614615
});
615616

617+
describe('default IAM role', () => {
618+
619+
test('Default role not in role list', () => {
620+
// GIVEN
621+
const clusterRole1 = new iam.Role(stack, 'clusterRole1', { assumedBy: new iam.ServicePrincipal('redshift.amazonaws.com') } );
622+
const defaultRole1 = new iam.Role(stack, 'defaultRole1', { assumedBy: new iam.ServicePrincipal('redshift.amazonaws.com') } );
623+
624+
expect(() => {
625+
new Cluster(stack, 'Redshift', {
626+
masterUser: {
627+
masterUsername: 'admin',
628+
},
629+
vpc,
630+
roles: [clusterRole1],
631+
defaultRole: defaultRole1,
632+
});
633+
}).toThrow(/Default role must be included in role list./);
634+
});
635+
636+
test('throws error when default role not attached to cluster when adding default role post creation', () => {
637+
const defaultRole1 = new iam.Role(stack, 'defaultRole1', { assumedBy: new iam.ServicePrincipal('redshift.amazonaws.com') } );
638+
const cluster = new Cluster(stack, 'Redshift', {
639+
masterUser: {
640+
masterUsername: 'admin',
641+
},
642+
vpc,
643+
});
644+
645+
expect(() => {
646+
cluster.addDefaultIamRole(defaultRole1);
647+
}).toThrow(/Default role must be associated to the Redshift cluster to be set as the default role./);
648+
});
649+
});
650+
616651
function testStack() {
617652
const newTestStack = new cdk.Stack(undefined, undefined, { env: { account: '12345', region: 'us-test-1' } });
618653
newTestStack.node.setContext('availability-zones:12345:us-test-1', ['us-test-1a', 'us-test-1b']);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"version": "29.0.0",
3+
"files": {
4+
"21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": {
5+
"source": {
6+
"path": "DefaultIamRoleIntegDefaultTestDeployAssert161B8D72.template.json",
7+
"packaging": "file"
8+
},
9+
"destinations": {
10+
"current_account-current_region": {
11+
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
12+
"objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json",
13+
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
14+
}
15+
}
16+
}
17+
},
18+
"dockerImages": {}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"Parameters": {
3+
"BootstrapVersion": {
4+
"Type": "AWS::SSM::Parameter::Value<String>",
5+
"Default": "/cdk-bootstrap/hnb659fds/version",
6+
"Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"
7+
}
8+
},
9+
"Rules": {
10+
"CheckBootstrapVersion": {
11+
"Assertions": [
12+
{
13+
"Assert": {
14+
"Fn::Not": [
15+
{
16+
"Fn::Contains": [
17+
[
18+
"1",
19+
"2",
20+
"3",
21+
"4",
22+
"5"
23+
],
24+
{
25+
"Ref": "BootstrapVersion"
26+
}
27+
]
28+
}
29+
]
30+
},
31+
"AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
32+
}
33+
]
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)