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(redshift): optionally reboot Clusters to apply parameter changes #22063

Merged
merged 25 commits into from
Feb 17, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8b8798c
inital implementation
dontirun Sep 13, 2022
68ebe28
add lambda handler
dontirun Sep 14, 2022
f262a52
documentation, tests, and bugfixes
Sep 15, 2022
6c1122b
Merge branch 'main' into feat-reboot-cluster-for-parameter-updates
Sep 30, 2022
c61f994
Merge branch 'main' into feat-reboot-cluster-for-parameter-updates
Oct 4, 2022
6fe9387
Merge branch 'main' into feat-reboot-cluster-for-parameter-updates
Oct 10, 2022
63400f6
fix: allow for enabling the reboot feature before the parameter group…
Oct 10, 2022
a5d1ee5
test: comment out assertions
Oct 10, 2022
153345a
chore: commit deleted asstes
Oct 10, 2022
f31b6dd
chore: update snapshot
Oct 11, 2022
a01c334
Merge branch 'main' into feat-reboot-cluster-for-parameter-updates
Oct 19, 2022
52916e6
tests: regenerate snapshot
Oct 19, 2022
c445b0f
chore: commenting out diff assets in integ test
Nov 16, 2022
26fbbaa
docs: add testing procedure to integ test
Nov 16, 2022
b732dcf
Merge branch 'main' into feat-reboot-cluster-for-parameter-updates
Nov 16, 2022
692d1ab
Merge branch 'main' into feat-reboot-cluster-for-parameter-updates
Dec 9, 2022
762f2bf
test: update assertions on integ test
Dec 14, 2022
dae7883
Merge branch 'main' into feat-reboot-cluster-for-parameter-updates
dontirun Dec 16, 2022
75ebd28
Merge branch 'main' into feat-reboot-cluster-for-parameter-updates
dontirun Jan 18, 2023
b6c323d
refactor: removing unknown-error from reboot actions
dontirun Feb 14, 2023
1c69185
Merge branch 'master' into feat-reboot-cluster-for-parameter-updates
dontirun Feb 14, 2023
80848a9
Update packages/@aws-cdk/aws-redshift/lib/cluster-parameter-change-re…
dontirun Feb 14, 2023
f91ffeb
refactor: use guard clause for clarity
Feb 16, 2023
a76b887
refactor: update guard clause
Feb 17, 2023
16ba85f
Merge branch 'main' into feat-reboot-cluster-for-parameter-updates
mergify[bot] Feb 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
documentation, tests, and bugfixes
  • Loading branch information
Arun Donti committed Sep 15, 2022
commit f262a52199410d22260659258517918ca3a9cf99
20 changes: 19 additions & 1 deletion packages/@aws-cdk/aws-redshift/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,25 @@ const cluster = new Cluster(this, 'Cluster', {
cluster.addToParameterGroup('enable_user_activity_logging', 'true');
```

## Rebooting for Parameter Updates

In most cases, existing clusters [must be manually rebooted](https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-parameter-groups.html) to apply parameter changes. You can automate parameter related reboots by setting the cluster's `rebootForParameterChanges` property to `true` , or by using `Cluster.enableRebootForParameterChanges()`.

```ts
declare const vpc: ec2.Vpc;

const cluster = new Cluster(this, 'Cluster', {
masterUser: {
masterUsername: 'admin',
masterPassword: cdk.SecretValue.unsafePlainText('tooshort'),
},
vpc,
});

cluster.addToParameterGroup('enable_user_activity_logging', 'true');
cluster.enableRebootForParameterChanges()
```

## Elastic IP

If you configure your cluster to be publicly accessible, you can optionally select an *elastic IP address* to use for the external IP address. An elastic IP address is a static IP address that is associated with your AWS account. You can use an elastic IP address to connect to your cluster from outside the VPC. An elastic IP address gives you the ability to change your underlying configuration without affecting the IP address that clients use to connect to your cluster. This approach can be helpful for situations such as recovery after a failure.
Expand Down Expand Up @@ -367,4 +386,3 @@ The elastic IP address is an external IP address for accessing the cluster outsi
### Attach Elastic IP after Cluster creation

In some cases, you might want to associate the cluster with an elastic IP address or change an elastic IP address that is associated with the cluster. To attach an elastic IP address after the cluster is created, first update the cluster so that it is not publicly accessible, then make it both publicly accessible and add an Elastic IP address in the same operation.

Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,49 @@ import { Redshift } from 'aws-sdk';

const redshift = new Redshift();

export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) {
export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise<void> {
if (event.RequestType !== 'Delete') {
return rebootClusterIfRequired(event);
return rebootClusterIfRequired(event.ResourceProperties?.ClusterId, event.ResourceProperties?.ParameterGroupName);
} else {
return;
}
}

async function rebootClusterIfRequired(event: AWSLambda.CloudFormationCustomResourceEvent) {
const clusterId = event.ResourceProperties?.ClusterId;
const parameterGroupName = event.ResourceProperties?.ParameterGroupName;
const clusterDetails = await redshift.describeClusters({ ClusterIdentifier: clusterId }).promise();
let found = false;
if (clusterDetails.Clusters?.[0].ClusterParameterGroups === undefined) {
throw new Error(`Unable to find any Parameter Groups associated with ClusterId "${clusterId}".`);
}
for (const group of clusterDetails.Clusters?.[0].ClusterParameterGroups) {
if (group.ParameterGroupName === parameterGroupName) {
found = true;
if (group.ParameterApplyStatus === 'pending-reboot') {
async function rebootClusterIfRequired(clusterId: string, parameterGroupName: string): Promise<void> {
return executeActionForStatus(await getApplyStatus());

// https://docs.aws.amazon.com/redshift/latest/APIReference/API_ClusterParameterStatus.html
async function executeActionForStatus(status: string, retryDurationMs?: number): Promise<void> {
await sleep(retryDurationMs ?? 0);
if (['pending-reboot', 'apply-deferred', 'apply-error', 'unknown-error'].includes(status)) {
dontirun marked this conversation as resolved.
Show resolved Hide resolved
try {
await redshift.rebootCluster({ ClusterIdentifier: clusterId }).promise();
} else if (group.ParameterApplyStatus === 'applying') {
await sleep(60000);
await rebootClusterIfRequired(event);
} catch (err) {
if ((<any>err).code === 'InvalidClusterState') {
dontirun marked this conversation as resolved.
Show resolved Hide resolved
return await executeActionForStatus(status, 30000);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought about doing some sort of backoff here, but found checking every 30 seconds to be sufficient in my testing.

} else {
throw err;
}
}
break;
return;
} else if (['applying', 'retry'].includes(status)) {
return executeActionForStatus(await getApplyStatus(), 30000);
}
return;
}
if (!found) {

async function getApplyStatus(): Promise<string> {
const clusterDetails = await redshift.describeClusters({ ClusterIdentifier: clusterId }).promise();
if (clusterDetails.Clusters?.[0].ClusterParameterGroups === undefined) {
throw new Error(`Unable to find any Parameter Groups associated with ClusterId "${clusterId}".`);
}
for (const group of clusterDetails.Clusters?.[0].ClusterParameterGroups) {
if (group.ParameterGroupName === parameterGroupName) {
return group.ParameterApplyStatus ?? 'retry';
}
}
throw new Error(`Unable to find Parameter Group named "${parameterGroupName}" associated with ClusterId "${clusterId}".`);
}
return;
}

function sleep(ms: number) {
Expand Down
15 changes: 11 additions & 4 deletions packages/@aws-cdk/aws-redshift/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as kms from '@aws-cdk/aws-kms';
import * as lambda from '@aws-cdk/aws-lambda';
import * as s3 from '@aws-cdk/aws-s3';
import * as secretsmanager from '@aws-cdk/aws-secretsmanager';
import { Duration, IResource, RemovalPolicy, Resource, SecretValue, Token, CustomResource, Stack, ArnFormat } from '@aws-cdk/core';
import { ArnFormat, CustomResource, Duration, IResource, Lazy, RemovalPolicy, Resource, SecretValue, Stack, Token } from '@aws-cdk/core';
import * as cr from '@aws-cdk/custom-resources';
import { Construct } from 'constructs';
import { DatabaseSecret } from './database-secret';
Expand Down Expand Up @@ -693,7 +693,7 @@ export class Cluster extends ClusterBase {
resources: ['*'],
}));
rebootFunction.addToRolePolicy(new iam.PolicyStatement({
actions: ['reshift:RebootCluster'],
actions: ['redshift:RebootCluster'],
resources: [
Stack.of(this).formatArn({
service: 'redshift',
Expand All @@ -710,9 +710,16 @@ export class Cluster extends ClusterBase {
resourceType: 'Custom::RedshiftClusterRebooter',
serviceToken: provider.serviceToken,
properties: {
ClusterId: this.cluster.getAtt('id'),
ClusterId: this.clusterName,
ParameterGroupName: this.parameterGroup.clusterParameterGroupName,
ParametersString: JSON.stringify(this.parameterGroup.parameters),
ParametersString: Lazy.string({
produce: () => {
if (!(this.parameterGroup instanceof ClusterParameterGroup)) {
throw new Error('Cannot enable reboot for parameter changes when using an imported parameter group.');
}
return JSON.stringify(this.parameterGroup.parameters);
},
}),
},
});
customResource.node.addDependency(this, this.parameterGroup);
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"aws-cdk-redshift-cluster-reboot-integ": {
"ExportsOutputRefClusterEB0386A796A0E3FE": "clustereb0386a7-dzxalxbye79k",
"ExportsOutputRefParameterGroup5E32DECBB33EA140": "aws-cdk-redshift-cluster-reboot-integ-parametergroup5e32decb-kuykp4syd337"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { Redshift } from 'aws-sdk';

const redshift = new Redshift();

export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise<void> {
if (event.RequestType !== 'Delete') {
return rebootClusterIfRequired(event.ResourceProperties?.ClusterId, event.ResourceProperties?.ParameterGroupName);
} else {
return;
}
}

async function rebootClusterIfRequired(clusterId: string, parameterGroupName: string): Promise<void> {
return executeActionForStatus(await getApplyStatus());

// https://docs.aws.amazon.com/redshift/latest/APIReference/API_ClusterParameterStatus.html
async function executeActionForStatus(status: string, retryDurationMs?: number): Promise<void> {
await sleep(retryDurationMs ?? 0);
if (['pending-reboot', 'apply-deferred', 'apply-error', 'unknown-error'].includes(status)) {
try {
await redshift.rebootCluster({ ClusterIdentifier: clusterId }).promise();
} catch (err) {
if ((<any>err).code === 'InvalidClusterState') {
return await executeActionForStatus(status, 30000);
} else {
throw err;
}
}
return;
} else if (['applying', 'retry'].includes(status)) {
return executeActionForStatus(await getApplyStatus(), 30000);
}
return;
}

async function getApplyStatus(): Promise<string> {
const clusterDetails = await redshift.describeClusters({ ClusterIdentifier: clusterId }).promise();
if (clusterDetails.Clusters?.[0].ClusterParameterGroups === undefined) {
throw new Error(`Unable to find any Parameter Groups associated with ClusterId "${clusterId}".`);
}
for (const group of clusterDetails.Clusters?.[0].ClusterParameterGroups) {
if (group.ParameterGroupName === parameterGroupName) {
return group.ParameterApplyStatus ?? 'retry';
}
}
throw new Error(`Unable to find Parameter Group named "${parameterGroupName}" associated with ClusterId "${clusterId}".`);
}
}

function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { Redshift } from 'aws-sdk';

const redshift = new Redshift();

export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise<void> {
if (event.RequestType !== 'Delete') {
return rebootClusterIfRequired(event.ResourceProperties?.ClusterId, event.ResourceProperties?.ParameterGroupName);
} else {
return;
}
}

async function rebootClusterIfRequired(clusterId: string, parameterGroupName: string): Promise<void> {
return executeActionForStatus(await getApplyStatus());

// https://docs.aws.amazon.com/redshift/latest/APIReference/API_ClusterParameterStatus.html
async function executeActionForStatus(status: string, retryDurationMs?: number): Promise<void> {
await sleep(retryDurationMs ?? 0);
if (['pending-reboot', 'apply-deferred', 'apply-error', 'unknown-error'].includes(status)) {
try {
await redshift.rebootCluster({ ClusterIdentifier: clusterId }).promise();
} catch (err) {
if ((<any>err).code === 'InvalidClusterState') {
return await executeActionForStatus(status, 30000);
} else {
throw err;
}
}
return;
} else if (['applying', 'retry'].includes(status)) {
return executeActionForStatus(await getApplyStatus(), 30000);
}
return;
}

async function getApplyStatus(): Promise<string> {
const clusterDetails = await redshift.describeClusters({ ClusterIdentifier: clusterId }).promise();
if (clusterDetails.Clusters?.[0].ClusterParameterGroups === undefined) {
throw new Error(`Unable to find any Parameter Groups associated with ClusterId "${clusterId}".`);
}
for (const group of clusterDetails.Clusters?.[0].ClusterParameterGroups) {
if (group.ParameterGroupName === parameterGroupName) {
return group.ParameterApplyStatus ?? 'retry';
}
}
throw new Error(`Unable to find Parameter Group named "${parameterGroupName}" associated with ClusterId "${clusterId}".`);
}
}

function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { Redshift } from 'aws-sdk';

const redshift = new Redshift();

export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise<void> {
if (event.RequestType !== 'Delete') {
return rebootClusterIfRequired(event.ResourceProperties?.ClusterId, event.ResourceProperties?.ParameterGroupName);
} else {
return;
}
}

async function rebootClusterIfRequired(clusterId: string, parameterGroupName: string) {
return executeActionForStatus(await getApplyStatus());

// https://docs.aws.amazon.com/redshift/latest/APIReference/API_ClusterParameterStatus.html
async function executeActionForStatus(status: string): Promise<void> {
if (['pending-reboot', 'apply-deferred', 'apply-error', 'unknown-error'].includes(status)) {
try {
await redshift.rebootCluster({ ClusterIdentifier: clusterId }).promise();
} catch (err) {
if ((<any>err).code === 'InvalidClusterState') {
await sleep(60000);
return await executeActionForStatus(await getApplyStatus());
} else {
throw err;
}
}
return;
} else if (['applying', 'retry'].includes(status)) {
await sleep(60000);
return executeActionForStatus(await getApplyStatus());
}
return;
}

async function getApplyStatus(): Promise<string> {
const clusterDetails = await redshift.describeClusters({ ClusterIdentifier: clusterId }).promise();
if (clusterDetails.Clusters?.[0].ClusterParameterGroups === undefined) {
throw new Error(`Unable to find any Parameter Groups associated with ClusterId "${clusterId}".`);
}
for (const group of clusterDetails.Clusters?.[0].ClusterParameterGroups) {
if (group.ParameterGroupName === parameterGroupName) {
return group.ParameterApplyStatus ?? 'retry';
}
}
throw new Error(`Unable to find Parameter Group named "${parameterGroupName}" associated with ClusterId "${clusterId}".`);
}
}

function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"version": "21.0.0",
"files": {
"631d177ea8a37a639d61917693cce9bbdb8e53862bc1ead871c88a235f8ef139": {
"source": {
"path": "asset.631d177ea8a37a639d61917693cce9bbdb8e53862bc1ead871c88a235f8ef139",
"packaging": "zip"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "631d177ea8a37a639d61917693cce9bbdb8e53862bc1ead871c88a235f8ef139.zip",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
},
"3b263c2ad043fd069ef446753788c36e595c82b51a70478e58258c8ef7471671": {
"source": {
"path": "asset.3b263c2ad043fd069ef446753788c36e595c82b51a70478e58258c8ef7471671",
"packaging": "zip"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "3b263c2ad043fd069ef446753788c36e595c82b51a70478e58258c8ef7471671.zip",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
},
"42ff6c40ab2191a9f141b53cff77b4d1a253b7af577e2d0c9a196cdc4316ee2f": {
"source": {
"path": "aws-cdk-redshift-cluster-create.template.json",
"packaging": "file"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "42ff6c40ab2191a9f141b53cff77b4d1a253b7af577e2d0c9a196cdc4316ee2f.json",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
}
},
"dockerImages": {}
}
Loading