Skip to content

Commit

Permalink
Opt-in region State Machine enhancements (#1231)
Browse files Browse the repository at this point in the history
* initial optin work

* initial optin work

* initial optin work

* initial optin work

* initial optin work

* initial optin work

* initial optin work discard

* initial optin work discard

* initial optin work fix run all accounts

* initial optin work exclude control tower

* initial optin work enable bug fix

* initial optin work enable bug fix

* initial optin work enable bug fix error check

* initial optin work enable add check

* fix eslint
  • Loading branch information
rjjaegeraws authored Jan 21, 2025
1 parent 3bb5b7f commit 33e4dfa
Show file tree
Hide file tree
Showing 8 changed files with 426 additions and 0 deletions.
29 changes: 29 additions & 0 deletions src/core/cdk/src/initial-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { CreateControlTowerAccountTask } from './tasks/create-control-tower-acco
import { CreateOrganizationAccountTask } from './tasks/create-organization-account-task';
import { CreateStackTask } from './tasks/create-stack-task';
import { RunAcrossAccountsTask } from './tasks/run-across-accounts-task';
import { EnableOptinRegionTask } from './tasks/enable-optin-region-task';
import { Construct } from 'constructs';
import * as fs from 'fs';
import * as sns from 'aws-cdk-lib/aws-sns';
Expand Down Expand Up @@ -623,6 +624,33 @@ export namespace InitialSetup {

installExecRolesInAccounts.iterator(installRolesTask);

// Opt in Region - Begin
const optinRegionsStateMachine = new sfn.StateMachine(this, `${props.acceleratorPrefix}OptinRegions_sm`, {
stateMachineName: `${props.acceleratorPrefix}OptinRegions_sm`,
definition: new EnableOptinRegionTask(this, 'OptinRegions', {
lambdaCode,
role: pipelineRole,
}),
});

const optinRegionTask = new tasks.StepFunctionsStartExecution(this, 'Enable Opt-in Regions', {
stateMachine: optinRegionsStateMachine,
integrationPattern: sfn.IntegrationPattern.RUN_JOB,
resultPath: sfn.JsonPath.DISCARD,
input: sfn.TaskInput.fromObject({
'accounts.$': '$.accounts',
'regions.$': '$.regions',
configRepositoryName: props.configRepositoryName,
'configFilePath.$': '$.configFilePath',
'configCommitId.$': '$.configCommitId',
'baseline.$': '$.baseline',
acceleratorPrefix: props.acceleratorPrefix,
assumeRoleName: props.stateMachineExecutionRole,
}),
});

// Opt in Region - End

const deleteVpcSfn = new sfn.StateMachine(this, 'Delete Default Vpcs Sfn', {
stateMachineName: `${props.acceleratorPrefix}DeleteDefaultVpcs_sfn`,
definition: new RunAcrossAccountsTask(this, 'DeleteDefaultVPCs', {
Expand Down Expand Up @@ -1131,6 +1159,7 @@ export namespace InitialSetup {
const commonDefinition = loadOrganizationsTask.startState
.next(loadAccountsTask)
.next(installExecRolesInAccounts)
.next(optinRegionTask)
.next(cdkBootstrapTask)
.next(deleteVpcTask)
.next(loadLimitsTask)
Expand Down
129 changes: 129 additions & 0 deletions src/core/cdk/src/tasks/enable-optin-region-task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/**
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
* with the License. A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
* OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
* and limitations under the License.
*/

import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as sfn from 'aws-cdk-lib/aws-stepfunctions';
import { CodeTask } from '@aws-accelerator/cdk-accelerator/src/stepfunction-tasks';
import { Construct } from 'constructs';

export namespace EnableOptinRegionTask {
export interface Props {
role: iam.IRole;
lambdaCode: lambda.Code;
waitSeconds?: number;
}
}

export class EnableOptinRegionTask extends sfn.StateMachineFragment {
readonly startState: sfn.State;
readonly endStates: sfn.INextable[];

constructor(scope: Construct, id: string, props: EnableOptinRegionTask.Props) {
super(scope, id);

const { role, lambdaCode, waitSeconds = 60 } = props;

role.addToPrincipalPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: ['*'],
actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'],
}),
);
role.addToPrincipalPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: ['*'],
actions: ['codepipeline:PutJobSuccessResult', 'codepipeline:PutJobFailureResult'],
}),
);

const createTaskResultPath = '$.enableOutput';
const createTaskResultLength = `${createTaskResultPath}.outputCount`;
const createTaskErrorCount = `${createTaskResultPath}.errorCount`;

const enableTask = new CodeTask(scope, `Start Optin Region`, {
resultPath: createTaskResultPath,
functionProps: {
role,
code: lambdaCode,
handler: 'index.enableOptinRegions.enable',
},
});

// Create Map task to iterate
const mapTask = new sfn.Map(this, `Enable Optin Region Map`, {
itemsPath: '$.accounts',
resultPath: sfn.JsonPath.DISCARD,
maxConcurrency: 15,
parameters: {
'accountId.$': '$$.Map.Item.Value',
'assumeRoleName.$': '$.assumeRoleName',
'configRepositoryName.$': '$.configRepositoryName',
'configFilePath.$': '$.configFilePath',
'configCommitId.$': '$.configCommitId',
'acceleratorPrefix.$': '$.acceleratorPrefix',
'baseline.$': '$.baseline',
},
});
mapTask.iterator(enableTask);

const verifyTaskResultPath = '$.verifyOutput';
const verifyTask = new CodeTask(scope, 'Verify Optin Region', {
resultPath: verifyTaskResultPath,
functionProps: {
role,
code: lambdaCode,
handler: 'index.enableOptinRegions.verify',
},
});

const waitTask = new sfn.Wait(scope, 'Wait for Optin Region Enabling', {
time: sfn.WaitTime.duration(cdk.Duration.seconds(waitSeconds)),
});

const pass = new sfn.Pass(this, 'Optin Region Enablement Succeeded');

const fail = new sfn.Fail(this, 'Optin Region Enablement Failed');

waitTask
.next(verifyTask)
.next(
new sfn.Choice(scope, 'Optin Region Enablement Done?')
.when(sfn.Condition.stringEquals(verifyTaskResultPath, 'SUCCESS'), pass)
.when(sfn.Condition.stringEquals(verifyTaskResultPath, 'IN_PROGRESS'), waitTask)
.otherwise(fail)
.afterwards(),
);

enableTask.next(
new sfn.Choice(scope, 'Optin Region Enablement Started?')
.when(
sfn.Condition.and(
sfn.Condition.numberEquals(createTaskResultLength, 0),
sfn.Condition.numberEquals(createTaskErrorCount, 0),
),
pass,
) // already enabled or skipped
.when(sfn.Condition.numberGreaterThan(createTaskResultLength, 0), waitTask) // processing
.when(sfn.Condition.numberGreaterThan(createTaskErrorCount, 0), fail)
.otherwise(fail)
.afterwards(),
);

this.startState = mapTask.startState;
this.endStates = fail.endStates;
}
}
124 changes: 124 additions & 0 deletions src/core/runtime/src/enable-optin-regions/enable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
* with the License. A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
* OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
* and limitations under the License.
*/

import { Account } from '@aws-accelerator/common/src/aws/account';
import { EC2 } from '@aws-accelerator/common/src/aws/ec2';
import { LoadConfigurationInput } from '../load-configuration-step';
import { STS } from '@aws-accelerator/common/src/aws/sts';
import { loadAcceleratorConfig } from '@aws-accelerator/common-config/src/load';
import { Organizations } from '@aws-accelerator/common/src/aws/organizations';
import { equalIgnoreCase } from '@aws-accelerator/common/src/util/common';

interface EnableOptinRegionInput extends LoadConfigurationInput {
accountId: string;
assumeRoleName: string;
}

export interface EnableOptinRegionOutput {
accountId: string;
optinRegionName: string;
assumeRoleName: string;
}

const CustomErrorMessage = [
{
code: 'AuthFailure',
message: 'Region Not Enabled',
},
{
code: 'OptInRequired',
message: 'Region not Opted-in',
},
];

const sts = new STS();
const organizations = new Organizations();
export const handler = async (input: EnableOptinRegionInput) => {
console.log(`Enabling Opt-in Region in account ...`);
console.log(JSON.stringify(input, null, 2));
const { accountId, assumeRoleName, configRepositoryName, configFilePath, configCommitId } = input;

// Retrieve Configuration from Code Commit with specific commitId
const acceleratorConfig = await loadAcceleratorConfig({
repositoryName: configRepositoryName,
filePath: configFilePath,
commitId: configCommitId,
});
const awsAccount = await organizations.getAccount(accountId);
if (!awsAccount) {
// This will never happen unless it is called explicitly with invalid AccountId
throw new Error(`Unable to retrieve account info from Organizations API for "${accountId}"`);
}

const supportedRegions = acceleratorConfig['global-options']['supported-regions'];

console.log(`${accountId}: ${JSON.stringify(supportedRegions, null, 2)}`);
const errors: string[] = [];
const credentials = await sts.getCredentialsForAccountAndRole(accountId, assumeRoleName);
const account = new Account(credentials, 'us-east-1');
const ec2 = new EC2(credentials, 'us-east-1');
const isControlTower = acceleratorConfig['global-options']['ct-baseline'];
const enabledRegions = await ec2.describeAllRegions();
const enabledOptinRegionList: EnableOptinRegionOutput[] = [];

if (!isControlTower) {
if (enabledRegions) {
const enabledRegionLookup = Object.fromEntries(enabledRegions.map(obj => [obj.RegionName, obj.OptInStatus]));

for (const region of supportedRegions) {
const enabledRegionStatus = enabledRegionLookup[region];

// If region is an opt-in region
if (enabledRegionStatus === 'not-opted-in') {
// Check to see if it is Enabling state. This could happen during a SM restart.
const optInRegionStatus = await account.getRegionOptinStatus(region);
if (optInRegionStatus.RegionOptStatus! === 'ENABLING') {
console.log(`Opt-in region '${region}' is already being enabled. Skipping.`);
enabledOptinRegionList.push({
accountId,
optinRegionName: region,
assumeRoleName,
});
continue;
}

console.log(`Enabling Opt-in region '${region}'`);
try {
await account.enableOptinRegion(region);
enabledOptinRegionList.push({
accountId,
optinRegionName: region,
assumeRoleName,
});
} catch (error: any) {
errors.push(
`${accountId}:${region}: ${error.code}: ${
CustomErrorMessage.find(cm => cm.code === error.code)?.message || error.message
}`,
);
continue;
}
} else if (enabledRegionStatus === 'opted-in') {
console.log(`${region} already opted-in`);
} else {
// opt-in-not-required
console.log(`${region} opt-in-not required`);
}
}
}
} else {
console.log(`Control Tower is enabled. Skipping Opt-in enablement.`);
}

return { enabledOptinRegionList, outputCount: enabledOptinRegionList.length, errors, errorCount: errors.length };
};
15 changes: 15 additions & 0 deletions src/core/runtime/src/enable-optin-regions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
* with the License. A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
* OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
* and limitations under the License.
*/

export { handler as enable } from './enable';
export { handler as verify } from './verify';
59 changes: 59 additions & 0 deletions src/core/runtime/src/enable-optin-regions/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
* with the License. A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
* OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
* and limitations under the License.
*/

import { Account } from '@aws-accelerator/common/src/aws/account';
import { STS } from '@aws-accelerator/common/src/aws/sts';
import { EnableOptinRegionOutput } from './enable';

interface StepInput {
enableOutput: OptinRegionList;
}

interface OptinRegionList {
enabledOptinRegionList: EnableOptinRegionOutput[];
}

export const handler = async (input: StepInput): Promise<string> => {
console.log(`Verifying status of enabled Optin Regions`);
console.log(JSON.stringify(input, null, 2));

const status: string[] = [];
const sts = new STS();

for (const enabledOptinRegion of input.enableOutput.enabledOptinRegionList) {
const credentials = await sts.getCredentialsForAccountAndRole(
enabledOptinRegion.accountId,
enabledOptinRegion.assumeRoleName,
);

const account = new Account(credentials, 'us-east-1');

const optInRegionStatus = await account.getRegionOptinStatus(enabledOptinRegion.optinRegionName);

status.push(optInRegionStatus.RegionOptStatus!);
}

// "ENABLED"|"ENABLING"|"DISABLING"|"DISABLED"|"ENABLED_BY_DEFAULT"|string;

const statusEnabling = status.filter(s => s === 'ENABLING');
if (statusEnabling && statusEnabling.length > 0) {
return 'IN_PROGRESS';
}

const statusDisabling = status.filter(s => s === 'DISABLING');
if (statusDisabling && statusDisabling.length > 0) {
return 'IN_PROGRESS';
}

return 'SUCCESS';
};
2 changes: 2 additions & 0 deletions src/core/runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import * as createOrganizationAccount from './create-organization-account';
import * as createStack from './create-stack';
import * as createStackSet from './create-stack-set';
import * as deleteDefaultVpcs from './delete-default-vpc';
import * as enableOptinRegions from './enable-optin-regions';
export {
createAccount,
createStack,
Expand All @@ -58,4 +59,5 @@ export {
deleteDefaultVpcs,
createConfigRecorder,
addTagsToSharedResources,
enableOptinRegions,
};
Loading

0 comments on commit 33e4dfa

Please sign in to comment.