-
Notifications
You must be signed in to change notification settings - Fork 235
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Opt-in region State Machine enhancements (#1231)
* 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
1 parent
3bb5b7f
commit 33e4dfa
Showing
8 changed files
with
426 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.