From 8f52dc7bd1ed4c0aff9b398684ff0b77a9f36f98 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Wed, 16 Sep 2020 13:45:37 +0530 Subject: [PATCH 01/25] Using DynamoDB for tracking Resources created in a stack --- src/core/cdk/src/initial-setup.ts | 13 ++ src/deployments/cdk/src/apps/phase-4.ts | 5 + .../deployments/central-endpoints/step-4.ts | 219 +++++++++++++++--- src/deployments/cdk/src/utils/context.ts | 2 + .../cdk/src/utils/static-resources.ts | 35 +++ .../src/route53/resolver-rule.ts | 1 + src/lib/common/src/aws/dynamodb.ts | 32 +++ 7 files changed, 271 insertions(+), 36 deletions(-) create mode 100644 src/deployments/cdk/src/utils/static-resources.ts diff --git a/src/core/cdk/src/initial-setup.ts b/src/core/cdk/src/initial-setup.ts index 74d3170d2..0ad6854ba 100644 --- a/src/core/cdk/src/initial-setup.ts +++ b/src/core/cdk/src/initial-setup.ts @@ -90,6 +90,18 @@ export namespace InitialSetup { encryption: dynamodb.TableEncryption.DEFAULT, }); + const resourceTable = new dynamodb.Table(this, 'Resources', { + tableName: createName({ + name: 'Resources', + suffixLength: 0, + }), + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING, + }, + encryption: dynamodb.TableEncryption.DEFAULT, + }); + // This is the maximum time before a build times out // The role used by the build should allow this session duration const buildTimeout = cdk.Duration.hours(4); @@ -552,6 +564,7 @@ export namespace InitialSetup { ACCELERATOR_STATE_MACHINE_NAME: props.stateMachineName, CONFIG_BRANCH_NAME: props.configBranchName, STACK_OUTPUT_TABLE_NAME: outputsTable.tableName, + STACK_RESOURCES_TABLE_NAME: resourceTable.tableName, }; const deployTask = new sfn.Task(this, `Deploy Phase ${phase}`, { // tslint:disable-next-line: deprecation diff --git a/src/deployments/cdk/src/apps/phase-4.ts b/src/deployments/cdk/src/apps/phase-4.ts index eeff9bc59..1b4c559fd 100644 --- a/src/deployments/cdk/src/apps/phase-4.ts +++ b/src/deployments/cdk/src/apps/phase-4.ts @@ -3,6 +3,7 @@ import * as securityHub from '../deployments/security-hub'; import * as cloudWatchDeployment from '../deployments/cloud-watch'; import * as centralEndpoints from '../deployments/central-endpoints'; import { Context } from '@aws-cdk/aws-stepfunctions'; +import { loadStaticResources } from '../utils/static-resources'; export interface RdgwArtifactsOutput { accountKey: string; @@ -42,6 +43,7 @@ export async function deploy({ acceleratorConfig, accounts, accountStacks, outpu /** * Associate Hosted Zones to VPC */ + const staticResources = await loadStaticResources(context.resourcesTableName); await centralEndpoints.step4({ accountStacks, config: acceleratorConfig, @@ -49,5 +51,8 @@ export async function deploy({ acceleratorConfig, accounts, accountStacks, outpu accounts, executionRole: context.acceleratorPipelineRoleName, assumeRole: context.acceleratorExecutionRoleName, + staticResources, + acceleratorExecutionRoleName: context.acceleratorExecutionRoleName, + resourcesTableName: context.resourcesTableName, }); } diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-4.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-4.ts index 09f35c4d6..4fb956731 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-4.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-4.ts @@ -6,6 +6,9 @@ import { HostedZoneOutputFinder } from '@aws-accelerator/common-outputs/src/host import { Account, getAccountId } from '../../utils/accounts'; import { AssociateHostedZones } from '@aws-accelerator/custom-resource-associate-hosted-zones'; import * as cdk from '@aws-cdk/core'; +import { StaticResource } from '../../utils/static-resources'; +import { STS } from '@aws-accelerator/common/src/aws/sts'; +import { DynamoDB } from '@aws-accelerator/common/src/aws/dynamodb'; export interface CentralEndpointsStep4Props { accountStacks: AccountStacks; @@ -14,13 +17,26 @@ export interface CentralEndpointsStep4Props { accounts: Account[]; executionRole: string; assumeRole: string; + staticResources: StaticResource[]; + acceleratorExecutionRoleName: string; + resourcesTableName: string; } /** * Associate VPC to Hosted Zones to Vpcs based on use-central-endpoints */ export async function step4(props: CentralEndpointsStep4Props) { - const { accountStacks, config, outputs, accounts, assumeRole, executionRole } = props; + const { + accountStacks, + config, + outputs, + accounts, + assumeRole, + executionRole, + staticResources, + acceleratorExecutionRoleName, + resourcesTableName, + } = props; const allVpcConfigs = config.getVpcConfigs(); const zonesConfig = config['global-options'].zones; const globalPrivateHostedZoneIds: string[] = []; @@ -42,7 +58,33 @@ export async function step4(props: CentralEndpointsStep4Props) { } } - const regionAssociationCounter: { [region: string]: number } = {}; + const masterAccountId = getAccountId(accounts, masterAccountKey)!; + + const sts = new STS(); + const masterAcctCredentials = await sts.getCredentialsForAccountAndRole( + masterAccountId, + acceleratorExecutionRoleName, + ); + + const dynamodb = new DynamoDB(masterAcctCredentials); + + const existingRegionResources: { [region: string]: string[] } = {}; + const updateStackResources: StaticResource[] = []; + const supportedregions = config['global-options']['supported-regions']; + + const regionalMaxSuffix: { [region: string]: number } = {}; + supportedregions.forEach(reg => { + const localSuffix = staticResources + .filter(sr => sr.resourceType === 'HostedZoneAssociation' && sr.region === reg) + .flatMap(r => r.suffix); + regionalMaxSuffix[reg] = localSuffix.length === 0 ? 1 : Math.max(...localSuffix); + }); + + supportedregions.forEach(reg => { + existingRegionResources[reg] = staticResources + .filter(sr => sr.region === reg && sr.resourceType === 'HostedZoneAssociation') + .flatMap(r => r.resources); + }); for (const { accountKey, vpcConfig } of allVpcConfigs) { let seperateGlobalHostedZonesAccount = true; @@ -71,14 +113,37 @@ export async function step4(props: CentralEndpointsStep4Props) { continue; } - if (regionAssociationCounter[vpcConfig.region]) { - regionAssociationCounter[vpcConfig.region] = ++regionAssociationCounter[vpcConfig.region]; - } else { - regionAssociationCounter[vpcConfig.region] = 1; + let suffix = regionalMaxSuffix[vpcConfig.region]; + const existingResources = staticResources.find( + sr => + sr.region === vpcConfig.region && + sr.suffix === suffix && + sr.resourceType === `HostedZoneAssociation` && + sr.accountKey === masterAccountKey, + ); + if (existingResources && existingResources.resources.length >= 2) { + regionalMaxSuffix[vpcConfig.region] = ++suffix; } - // Includes max of 190 VPCs since we need 1 resource per VPC and 3 for Custom Resource. - const stackSuffix = `HostedZonesAssc-${Math.ceil(regionAssociationCounter[vpcConfig.region] / 190)}`; + let stackSuffix = `HostedZonesAssc-${suffix}`; + let updateDbRequired = true; + const constructName = `AssociateHostedZones-${accountKey}-${vpcConfig.name}-${vpcConfig.region}`; + const phzConstructName = `AssociatePrivateZones-${accountKey}-${vpcConfig.name}-${vpcConfig.region}`; + if (existingRegionResources[vpcConfig.region].includes(constructName)) { + updateDbRequired = false; + const regionStacks = staticResources.filter( + sr => + sr.region === vpcConfig.region && + sr.resourceType === 'HostedZoneAssociation' && + sr.accountKey === masterAccountKey, + ); + for (const rs of regionStacks) { + if (rs.resources.includes(constructName)) { + stackSuffix = `HostedZonesAssc-${rs.suffix}`; + break; + } + } + } const accountStack = accountStacks.tryGetOrCreateAccountStack(masterAccountKey, vpcConfig.region, stackSuffix); if (!accountStack) { @@ -98,44 +163,126 @@ export async function step4(props: CentralEndpointsStep4Props) { hostedZoneIds.push(...globalPrivateHostedZoneIds); } const hostedZoneAccountId = getAccountId(accounts, zoneConfig.account)!; - - new AssociateHostedZones( - accountStack, - `AssociateHostedZones-${accountKey}-${vpcConfig.name}-${vpcConfig.region}`, - { - assumeRoleName: assumeRole, - vpcAccountId, - vpcName: vpcConfig.name, - vpcId: vpcOutput.vpcId, - vpcRegion: vpcConfig.region, - hostedZoneAccountId, - hostedZoneIds, - roleArn: `arn:aws:iam::${cdk.Aws.ACCOUNT_ID}:role/${executionRole}`, - }, - ); + new AssociateHostedZones(accountStack, constructName, { + assumeRoleName: assumeRole, + vpcAccountId, + vpcName: vpcConfig.name, + vpcId: vpcOutput.vpcId, + vpcRegion: vpcConfig.region, + hostedZoneAccountId, + hostedZoneIds, + roleArn: `arn:aws:iam::${cdk.Aws.ACCOUNT_ID}:role/${executionRole}`, + }); } else { console.warn(`No Central VPC found for region "${vpcConfig.region}"`); } if (seperateGlobalHostedZonesAccount) { const hostedZoneAccountId = getAccountId(accounts, centralZoneConfig?.account!)!; + new AssociateHostedZones(accountStack, phzConstructName, { + assumeRoleName: assumeRole, + vpcAccountId, + vpcName: vpcConfig.name, + vpcId: vpcOutput.vpcId, + vpcRegion: vpcConfig.region, + hostedZoneAccountId, + hostedZoneIds: globalPrivateHostedZoneIds, + roleArn: `arn:aws:iam::${cdk.Aws.ACCOUNT_ID}:role/${executionRole}`, + }); + } - new AssociateHostedZones( - accountStack, - `AssociatePrivateZones-${accountKey}-${vpcConfig.name}-${vpcConfig.region}`, - { - assumeRoleName: assumeRole, - vpcAccountId, - vpcName: vpcConfig.name, - vpcId: vpcOutput.vpcId, - vpcRegion: vpcConfig.region, - hostedZoneAccountId, - hostedZoneIds: globalPrivateHostedZoneIds, - roleArn: `arn:aws:iam::${cdk.Aws.ACCOUNT_ID}:role/${executionRole}`, - }, + // Update stackResources Object if new resource came + if (updateDbRequired) { + const currentSuffixIndex = staticResources.findIndex( + sr => + sr.region === vpcConfig.region && + sr.suffix === suffix && + sr.resourceType === 'HostedZoneAssociation' && + sr.accountKey === masterAccountKey, + ); + const localUpdateIndex = updateStackResources.findIndex( + sr => + sr.region === vpcConfig.region && + sr.suffix === suffix && + sr.resourceType === 'HostedZoneAssociation' && + sr.accountKey === masterAccountKey, ); + const vpcAssociationResources = [constructName]; + if (seperateGlobalHostedZonesAccount) { + vpcAssociationResources.push(phzConstructName); + } + if (currentSuffixIndex === -1) { + staticResources.push({ + accountKey: masterAccountKey, + id: `AssociateHostedZones-${vpcConfig.region}-${masterAccountKey}-${suffix}`, + region: vpcConfig.region, + resourceType: 'HostedZoneAssociation', + resources: vpcAssociationResources, + suffix, + }); + } else { + const currentResourcesObject = staticResources[currentSuffixIndex]; + currentResourcesObject.resources.push(...vpcAssociationResources); + staticResources[currentSuffixIndex] = currentResourcesObject; + } + if (localUpdateIndex === -1) { + updateStackResources.push({ + accountKey: masterAccountKey, + id: `AssociateHostedZones-${vpcConfig.region}-${masterAccountKey}-${suffix}`, + region: vpcConfig.region, + resourceType: 'HostedZoneAssociation', + resources: vpcAssociationResources, + suffix, + }); + } else { + const currentResourcesObject = updateStackResources[currentSuffixIndex]; + currentResourcesObject.resources.push(...vpcAssociationResources); + updateStackResources[currentSuffixIndex] = currentResourcesObject; + } } } + + for (const staticResource of updateStackResources) { + const updateExpression = dynamodb.getUpdateValueInput([ + { + key: 'a', + name: 'accountKey', + type: 'S', + value: staticResource.accountKey, + }, + { + key: 'r', + name: 'region', + type: 'S', + value: staticResource.region, + }, + { + key: 'p', + name: 'suffix', + type: 'N', + value: `${staticResource.suffix}`, + }, + { + key: 'res', + name: 'resources', + type: 'S', + value: JSON.stringify(staticResource.resources), + }, + { + key: 'rt', + name: 'resourceType', + type: 'S', + value: staticResource.resourceType, + }, + ]); + await dynamodb.updateItem({ + TableName: resourcesTableName, + Key: { + id: { S: staticResource.id }, + }, + ...updateExpression, + }); + } } function getHostedZoneIds( diff --git a/src/deployments/cdk/src/utils/context.ts b/src/deployments/cdk/src/utils/context.ts index b8be11b93..85d989386 100644 --- a/src/deployments/cdk/src/utils/context.ts +++ b/src/deployments/cdk/src/utils/context.ts @@ -15,6 +15,7 @@ export interface Context { acceleratorStateMachineName: string; configRootFilePath: string; installerVersion: string; + resourcesTableName: string; } export function loadContext(): Context { @@ -41,5 +42,6 @@ export function loadContext(): Context { acceleratorStateMachineName: process.env.ACCELERATOR_STATE_MACHINE_NAME!, configRootFilePath: process.env.CONFIG_ROOT_FILE_PATH!, installerVersion: process.env.INSTALLER_VERSION!, + resourcesTableName: process.env.STACK_RESOURCES_TABLE_NAME!, }; } diff --git a/src/deployments/cdk/src/utils/static-resources.ts b/src/deployments/cdk/src/utils/static-resources.ts new file mode 100644 index 000000000..a5c5e21d1 --- /dev/null +++ b/src/deployments/cdk/src/utils/static-resources.ts @@ -0,0 +1,35 @@ +import { DynamoDB } from '@aws-accelerator/common/src/aws/dynamodb'; + +export interface StaticResource { + id: string; + region: string; + accountKey: string; + suffix: number; + resourceType: string; + resources: string[]; +} + +const dynamodb = new DynamoDB(); + +export async function loadStaticResources(tableName: string): Promise { + const staticResources: StaticResource[] = []; + const staticResourcesResponse = await dynamodb.scan({ + TableName: tableName, + }); + if (!staticResourcesResponse) { + console.warn(`Did not find outputs in DynamoDB table "${tableName}"`); + return staticResources; + } + for (const item of staticResourcesResponse) { + const cVal: StaticResource = { + accountKey: item.accountKey.S!, + id: item.id.S!, + region: item.region.S!, + resourceType: item.resourceType.S!, + resources: JSON.parse(item.resources.S!), + suffix: parseInt(item.suffix.N!, 10), + }; + staticResources.push(cVal); + } + return staticResources; +} diff --git a/src/lib/cdk-constructs/src/route53/resolver-rule.ts b/src/lib/cdk-constructs/src/route53/resolver-rule.ts index 4fe4598d0..d5b97777c 100644 --- a/src/lib/cdk-constructs/src/route53/resolver-rule.ts +++ b/src/lib/cdk-constructs/src/route53/resolver-rule.ts @@ -23,6 +23,7 @@ export class ResolverRule extends cdk.Construct { port: '53', })); + // TODO: Use custom resource to create Rule and Association to handle delete action this.rule = new r53Resolver.CfnResolverRule(this, 'Rule', { domainName: props.domain, ruleType: props.ruleType, diff --git a/src/lib/common/src/aws/dynamodb.ts b/src/lib/common/src/aws/dynamodb.ts index 8b4430e91..0919bdf0d 100644 --- a/src/lib/common/src/aws/dynamodb.ts +++ b/src/lib/common/src/aws/dynamodb.ts @@ -2,6 +2,13 @@ import aws from './aws-client'; import * as dynamodb from 'aws-sdk/clients/dynamodb'; import { throttlingBackOff } from './backoff'; +interface Attribute { + key: string; + value: string; + name: string; + type: 'S' | 'N' | 'B'; +} + export class DynamoDB { private readonly client: aws.DynamoDB; @@ -24,6 +31,7 @@ export class DynamoDB { let token: dynamodb.Key | undefined; // TODO: Use common listgenerator when this api supports nextToken do { + // TODO: Use DynamoDB.Converter for scan and Query const response = await throttlingBackOff(() => this.client.scan(props).promise()); token = response.LastEvaluatedKey; props.ExclusiveStartKey = token; @@ -59,4 +67,28 @@ export class DynamoDB { async updateItem(props: dynamodb.UpdateItemInput): Promise { await throttlingBackOff(() => this.client.updateItem(props).promise()); } + + getUpdateValueInput(attributes: Attribute[]) { + if (attributes.length === 0) { + return; + } + const expAttributeNames: aws.DynamoDB.ExpressionAttributeNameMap = {}; + const expAttributeValues: aws.DynamoDB.ExpressionAttributeValueMap = {}; + let updateExpression: string = 'set '; + for (const att of attributes) { + const attributeValue: aws.DynamoDB.AttributeValue = {}; + expAttributeNames[`#${att.key}`] = att.name; + attributeValue[att.type] = att.value; + expAttributeValues[`:${att.key}`] = attributeValue; + updateExpression += `#${att.key} = :${att.key},`; + } + // Remove "," if exists as last character + updateExpression = updateExpression.endsWith(',') ? updateExpression.slice(0, -1) : updateExpression; + + return { + ExpressionAttributeNames: expAttributeNames, + UpdateExpression: updateExpression, + ExpressionAttributeValues: expAttributeValues, + }; + } } From 65b1c95be349eff6db65c6788fc7ad5508b4b1fe Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Wed, 16 Sep 2020 14:14:49 +0530 Subject: [PATCH 02/25] Adding TODO --- src/lib/custom-resources/cdk-associate-hosted-zones/cdk/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/custom-resources/cdk-associate-hosted-zones/cdk/index.ts b/src/lib/custom-resources/cdk-associate-hosted-zones/cdk/index.ts index b7c146276..b1330a88a 100644 --- a/src/lib/custom-resources/cdk-associate-hosted-zones/cdk/index.ts +++ b/src/lib/custom-resources/cdk-associate-hosted-zones/cdk/index.ts @@ -46,6 +46,7 @@ export class AssociateHostedZones extends cdk.Construct { return existing as lambda.Function; } + // TODO: Use existing Lambda function to avoid creating multiple Lambda function in same account and region in different stacks const lambdaPath = require.resolve('@aws-accelerator/custom-resource-associate-hosted-zones-runtime'); const lambdaDir = path.dirname(lambdaPath); From 1f5e8dfab85fca983567fe979971de64b8d63291 Mon Sep 17 00:00:00 2001 From: Naveen Koppula <43773714+naveenkoppula@users.noreply.github.com> Date: Wed, 16 Sep 2020 18:30:27 +0530 Subject: [PATCH 03/25] fix(core): Setting sleep before SM execution on move account (#391) --- .../src/ou-validation-events/move-account-organization.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/deployments/runtime/src/ou-validation-events/move-account-organization.ts b/src/deployments/runtime/src/ou-validation-events/move-account-organization.ts index 6e189c56c..da2601dd6 100644 --- a/src/deployments/runtime/src/ou-validation-events/move-account-organization.ts +++ b/src/deployments/runtime/src/ou-validation-events/move-account-organization.ts @@ -398,7 +398,14 @@ async function updateConfig(props: { account: org.Account; destinationOrg: Organ return 'SUCCESS'; } +function sleep(milliseconds: number): Promise { + return new Promise(resolve => setTimeout(resolve, milliseconds)); +} + async function startStateMachine(stateMachineArn: string): Promise { + // Setting 2 mins sleep before SM execution on Successfull account move. + await sleep(2 * 60 * 1000); + const runningExecutions = await stepfunctions.listExecutions({ stateMachineArn, statusFilter: 'RUNNING', From 37e5d90010ac95a9a5a3f700dcf633bdeafde14a Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Wed, 16 Sep 2020 21:19:20 +0530 Subject: [PATCH 04/25] Create Rulea and Associate Resolver Rule to custom resource --- src/core/cdk/src/initial-setup.ts | 13 -- src/deployments/cdk/package.json | 2 + src/deployments/cdk/src/apps/phase--1.ts | 6 + src/deployments/cdk/src/apps/phase-4.ts | 4 - .../deployments/central-endpoints/outputs.ts | 2 + .../deployments/central-endpoints/step-2.ts | 73 ++++---- .../deployments/central-endpoints/step-3.ts | 44 ++++- .../deployments/central-endpoints/step-4.ts | 166 +++++------------- .../iam/central-endpoints-deployment-roles.ts | 60 +++++++ .../cdk/src/deployments/iam/index.ts | 1 + src/deployments/cdk/src/utils/context.ts | 2 - .../cdk/src/utils/static-resources.ts | 35 ---- src/lib/common-outputs/src/static-resource.ts | 40 +++++ .../cdk-associate-resolver-rules/README.md | 36 ++++ .../cdk-associate-resolver-rules/cdk/index.ts | 56 ++++++ .../cdk-associate-resolver-rules/package.json | 24 +++ .../runtime/.gitignore | 1 + .../runtime/package.json | 31 ++++ .../runtime/src/index.ts | 120 +++++++++++++ .../runtime/tsconfig.json | 15 ++ .../runtime/webpack.config.ts | 4 + .../tsconfig.json | 21 +++ .../cdk-create-resolver-rule/README.md | 36 ++++ .../cdk-create-resolver-rule/cdk/index.ts | 63 +++++++ .../cdk-create-resolver-rule/package.json | 24 +++ .../runtime/.gitignore | 1 + .../runtime/package.json | 31 ++++ .../runtime/src/index.ts | 138 +++++++++++++++ .../runtime/tsconfig.json | 15 ++ .../runtime/webpack.config.ts | 4 + .../cdk-create-resolver-rule/tsconfig.json | 21 +++ 31 files changed, 875 insertions(+), 214 deletions(-) create mode 100644 src/deployments/cdk/src/deployments/iam/central-endpoints-deployment-roles.ts delete mode 100644 src/deployments/cdk/src/utils/static-resources.ts create mode 100644 src/lib/common-outputs/src/static-resource.ts create mode 100644 src/lib/custom-resources/cdk-associate-resolver-rules/README.md create mode 100644 src/lib/custom-resources/cdk-associate-resolver-rules/cdk/index.ts create mode 100644 src/lib/custom-resources/cdk-associate-resolver-rules/package.json create mode 100644 src/lib/custom-resources/cdk-associate-resolver-rules/runtime/.gitignore create mode 100644 src/lib/custom-resources/cdk-associate-resolver-rules/runtime/package.json create mode 100644 src/lib/custom-resources/cdk-associate-resolver-rules/runtime/src/index.ts create mode 100644 src/lib/custom-resources/cdk-associate-resolver-rules/runtime/tsconfig.json create mode 100644 src/lib/custom-resources/cdk-associate-resolver-rules/runtime/webpack.config.ts create mode 100644 src/lib/custom-resources/cdk-associate-resolver-rules/tsconfig.json create mode 100644 src/lib/custom-resources/cdk-create-resolver-rule/README.md create mode 100644 src/lib/custom-resources/cdk-create-resolver-rule/cdk/index.ts create mode 100644 src/lib/custom-resources/cdk-create-resolver-rule/package.json create mode 100644 src/lib/custom-resources/cdk-create-resolver-rule/runtime/.gitignore create mode 100644 src/lib/custom-resources/cdk-create-resolver-rule/runtime/package.json create mode 100644 src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts create mode 100644 src/lib/custom-resources/cdk-create-resolver-rule/runtime/tsconfig.json create mode 100644 src/lib/custom-resources/cdk-create-resolver-rule/runtime/webpack.config.ts create mode 100644 src/lib/custom-resources/cdk-create-resolver-rule/tsconfig.json diff --git a/src/core/cdk/src/initial-setup.ts b/src/core/cdk/src/initial-setup.ts index 0ad6854ba..74d3170d2 100644 --- a/src/core/cdk/src/initial-setup.ts +++ b/src/core/cdk/src/initial-setup.ts @@ -90,18 +90,6 @@ export namespace InitialSetup { encryption: dynamodb.TableEncryption.DEFAULT, }); - const resourceTable = new dynamodb.Table(this, 'Resources', { - tableName: createName({ - name: 'Resources', - suffixLength: 0, - }), - partitionKey: { - name: 'id', - type: dynamodb.AttributeType.STRING, - }, - encryption: dynamodb.TableEncryption.DEFAULT, - }); - // This is the maximum time before a build times out // The role used by the build should allow this session duration const buildTimeout = cdk.Duration.hours(4); @@ -564,7 +552,6 @@ export namespace InitialSetup { ACCELERATOR_STATE_MACHINE_NAME: props.stateMachineName, CONFIG_BRANCH_NAME: props.configBranchName, STACK_OUTPUT_TABLE_NAME: outputsTable.tableName, - STACK_RESOURCES_TABLE_NAME: resourceTable.tableName, }; const deployTask = new sfn.Task(this, `Deploy Phase ${phase}`, { // tslint:disable-next-line: deprecation diff --git a/src/deployments/cdk/package.json b/src/deployments/cdk/package.json index 888d43a28..6fb9e80da 100644 --- a/src/deployments/cdk/package.json +++ b/src/deployments/cdk/package.json @@ -81,6 +81,8 @@ "@aws-accelerator/custom-resource-ssm-session-manager-document": "workspace:^0.0.1", "@aws-accelerator/custom-resource-vpc-default-security-group": "workspace:^0.0.1", "@aws-accelerator/custom-resource-associate-hosted-zones": "workspace:^0.0.1", + "@aws-accelerator/custom-resource-associate-resolver-rules": "workspace:^0.0.1", + "@aws-accelerator/custom-resource-create-resolver-rule": "workspace:^0.0.1", "@aws-cdk/aws-accessanalyzer": "1.46.0", "@aws-cdk/aws-autoscaling": "1.46.0", "@aws-cdk/aws-budgets": "1.46.0", diff --git a/src/deployments/cdk/src/apps/phase--1.ts b/src/deployments/cdk/src/apps/phase--1.ts index 0894888dc..f390c4bb7 100644 --- a/src/deployments/cdk/src/apps/phase--1.ts +++ b/src/deployments/cdk/src/apps/phase--1.ts @@ -91,4 +91,10 @@ export async function deploy({ acceleratorConfig, accountStacks, accounts }: Pha accountStacks, accounts, }); + + // Creates role for Resource cleanup custom resource + await globalRoles.createCentralEndpointDeploymentRole({ + accountStacks, + config: acceleratorConfig, + }); } diff --git a/src/deployments/cdk/src/apps/phase-4.ts b/src/deployments/cdk/src/apps/phase-4.ts index 1b4c559fd..17b99550d 100644 --- a/src/deployments/cdk/src/apps/phase-4.ts +++ b/src/deployments/cdk/src/apps/phase-4.ts @@ -43,7 +43,6 @@ export async function deploy({ acceleratorConfig, accounts, accountStacks, outpu /** * Associate Hosted Zones to VPC */ - const staticResources = await loadStaticResources(context.resourcesTableName); await centralEndpoints.step4({ accountStacks, config: acceleratorConfig, @@ -51,8 +50,5 @@ export async function deploy({ acceleratorConfig, accounts, accountStacks, outpu accounts, executionRole: context.acceleratorPipelineRoleName, assumeRole: context.acceleratorExecutionRoleName, - staticResources, - acceleratorExecutionRoleName: context.acceleratorExecutionRoleName, - resourcesTableName: context.resourcesTableName, }); } diff --git a/src/deployments/cdk/src/deployments/central-endpoints/outputs.ts b/src/deployments/cdk/src/deployments/central-endpoints/outputs.ts index 6c9cf713d..839202576 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/outputs.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/outputs.ts @@ -1,3 +1,5 @@ import { HostedZoneOutput } from '@aws-accelerator/common-outputs/src/hosted-zone'; +import { StaticResourcesOutput } from '@aws-accelerator/common-outputs/src/static-resource'; import { createCfnStructuredOutput } from '../../common/structured-output'; export const CfnHostedZoneOutput = createCfnStructuredOutput(HostedZoneOutput); +export const CfnStaticResourcesOutput = createCfnStructuredOutput(StaticResourcesOutput); diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts index 66f6b90fe..f4c131857 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts @@ -13,6 +13,8 @@ import { JsonOutputValue } from '../../common/json-output'; import { Account, getAccountId } from '../../utils/accounts'; import * as ram from '@aws-cdk/aws-ram'; import { createName } from '@aws-accelerator/cdk-accelerator/src/core/accelerator-name-generator'; +import { CreateResolverRule } from '@aws-accelerator/custom-resource-create-resolver-rule'; +import { IamRoleOutputFinder } from '@aws-accelerator/common-outputs/src/iam-role'; export interface CentralEndpointsStep2Props { accountStacks: AccountStacks; @@ -67,6 +69,15 @@ export async function step2(props: CentralEndpointsStep2Props) { continue; } + const roleOutput = IamRoleOutputFinder.tryFindOneByName({ + outputs, + accountKey, + roleKey: 'CentralEndpointDeployment', + }); + if (!roleOutput) { + continue; + } + const subnetIds = vpcOutput.subnets.filter(s => s.subnetName === resolversConfig.subnet).map(s => s.subnetId); if (subnetIds.length === 0) { console.error( @@ -75,17 +86,21 @@ export async function step2(props: CentralEndpointsStep2Props) { continue; } - if (accountRulesCounter[`${accountKey}-${vpcConfig.region}`]) { - accountRulesCounter[`${accountKey}-${vpcConfig.region}`] = ++accountRulesCounter[ - `${accountKey}-${vpcConfig.region}` - ]; + let stackSuffix: string; + if (!!zonesConfig.find(zc => zc.account === accountKey && zc["resolver-vpc"] === vpcConfig.name && zc.region === vpcConfig.region)) { + stackSuffix= `EndpointsRules-${vpcConfig.name}`; } else { - accountRulesCounter[`${accountKey}-${vpcConfig.region}`] = 1; + if (accountRulesCounter[`${accountKey}-${vpcConfig.region}`]) { + accountRulesCounter[`${accountKey}-${vpcConfig.region}`] = ++accountRulesCounter[ + `${accountKey}-${vpcConfig.region}` + ]; + } else { + accountRulesCounter[`${accountKey}-${vpcConfig.region}`] = 1; + } + // Includes max of 10 VPCs, since we need max 8 resources for one VPC + stackSuffix= `EndpointsRules-${Math.ceil(accountRulesCounter[`${accountKey}-${vpcConfig.region}`] / 10)}`; } - // Includes max of 15 VPCs, since we need max 11 resources for one VPC - const stackSuffix = `EndpointsRules-${Math.ceil(accountRulesCounter[`${accountKey}-${vpcConfig.region}`] / 15)}`; - const accountStack = accountStacks.tryGetOrCreateAccountStack(accountKey, vpcConfig.region, stackSuffix); if (!accountStack) { console.error(`Cannot find account stack ${accountKey}: ${vpcConfig.region}, while deploying Resolver Endpoints`); @@ -121,18 +136,14 @@ export async function step2(props: CentralEndpointsStep2Props) { // For each on-premise domain defined in the parameters file, create a Resolver rule which points to the specified IP's for (const onPremRuleConfig of vpcConfig['on-premise-rules'] || []) { - const rule = new ResolverRule( - accountStack, - `${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}-on-prem-phz-rule`, - { - domain: onPremRuleConfig.zone, - endpoint: r53ResolverEndpoints.outboundEndpointRef, - ipAddresses: onPremRuleConfig['outbound-ips'], - ruleType: 'FORWARD', - name: `${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}-phz-rule`, - vpcId: vpcOutput.vpcId, - }, - ); + const rule = new CreateResolverRule(accountStack, `${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}-on-prem-phz-rule`, { + domainName: onPremRuleConfig.zone, + resolverEndpointId: r53ResolverEndpoints.outboundEndpointRef!, + roleArn: roleOutput.roleArn, + targetIps: onPremRuleConfig['outbound-ips'], + vpcId: vpcOutput.vpcId, + name: `${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}-on-prem-phz-rule`, + }); rule.node.addDependency(r53ResolverEndpoints); onPremRules.push(rule.ruleId); } @@ -156,18 +167,16 @@ export async function step2(props: CentralEndpointsStep2Props) { continue; } madIPs = madOutput[0].dnsIps.split(','); - const madRule = new ResolverRule( - accountStack, - `${domainToName(mad['dns-domain'])}-${vpcConfig.name}-phz-rule`, - { - domain: mad['dns-domain'], - endpoint: r53ResolverEndpoints.outboundEndpointRef, - ipAddresses: madIPs, - ruleType: 'FORWARD', - name: `${domainToName(mad['dns-domain'])}-${vpcConfig.name}-mad-phz-rule`, - vpcId: vpcOutput.vpcId, - }, - ); + + const madRule = new CreateResolverRule(accountStack, `${domainToName(mad['dns-domain'])}-${vpcConfig.name}-phz-rule`, { + domainName: mad['dns-domain'], + resolverEndpointId: r53ResolverEndpoints.outboundEndpointRef!, + roleArn: roleOutput.roleArn, + targetIps: madIPs, + vpcId: vpcOutput.vpcId, + name: `${domainToName(mad['dns-domain'])}-${vpcConfig.name}-mad-phz-rule`, + }); + madRule.node.addDependency(r53ResolverEndpoints.outboundEndpoint!); madRules.push(madRule.ruleId); } resolverRulesOutput.madRules = madRules; diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-3.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-3.ts index 6977d280b..0ab13d992 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-3.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-3.ts @@ -1,8 +1,16 @@ import { AccountStacks } from '../../common/account-stacks'; import { getStackJsonOutput, ResolversOutput, StackOutput } from '@aws-accelerator/common-outputs/src/stack-output'; +import { AssociateResolverRules } from '@aws-accelerator/custom-resource-associate-resolver-rules'; import * as c from '@aws-accelerator/common-config'; import * as route53resolver from '@aws-cdk/aws-route53resolver'; +import { IamRoleOutputFinder } from '@aws-accelerator/common-outputs/src/iam-role'; import { VpcOutputFinder } from '@aws-accelerator/common-outputs/src/vpc'; +import { StaticResourcesOutput, StaticResourcesOutputFinder } from '@aws-accelerator/common-outputs/src/static-resource'; + + +// Changing this will result to redeploy most of the stack +const MAX_RESOURCES_IN_STACK = 2; +const RESOURCE_TYPE = 'ResolverRuleAssociation'; export interface CentralEndpointsStep3Props { accountStacks: AccountStacks; @@ -17,6 +25,20 @@ export async function step3(props: CentralEndpointsStep3Props) { const { accountStacks, config, outputs } = props; const allVpcConfigs = config.getVpcConfigs(); const accountRulesCounter: { [accountKey: string]: number } = {}; + const supportedregions = config['global-options']['supported-regions']; + + const allStaticResources: StaticResourcesOutput[] = StaticResourcesOutputFinder.findAll({ + outputs, + }).filter(sr => sr.resourceType === RESOURCE_TYPE); + + const accountStaticResourcesConfig: { [accountKey: string]: StaticResourcesOutput[]} = {}; + const accountRegionExistingResources: { [accountKey: string]: { + [region: string]: string[] + }} = {}; + const accountRegionMaxSuffix: { [accountKey: string]: { + [region: string]: number + }} = {}; + for (const { accountKey, vpcConfig } of allVpcConfigs) { const centralPhzConfig = config['global-options'].zones.find(zc => zc.region === vpcConfig.region); if (!vpcConfig['use-central-endpoints']) { @@ -84,7 +106,7 @@ export async function step3(props: CentralEndpointsStep3Props) { } // Includes max of 50 VPCs, since we need 3 resource per VPC. - const stackSuffix = `RulesAssc-${Math.ceil(accountRulesCounter[`${accountKey}-${vpcConfig.region}`] / 50)}`; + const stackSuffix = `RulesAssc-${Math.ceil(accountRulesCounter[`${accountKey}-${vpcConfig.region}`] / 150)}`; const accountStack = accountStacks.tryGetOrCreateAccountStack(accountKey, vpcConfig.region, stackSuffix); if (!accountStack) { @@ -92,12 +114,20 @@ export async function step3(props: CentralEndpointsStep3Props) { continue; } - const ruleIds = [...resolverRegionoutputs.rules?.madRules!, ...resolverRegionoutputs.rules?.onPremRules!]; - for (const ruleId of ruleIds) { - new route53resolver.CfnResolverRuleAssociation(accountStack, `Rule-Association-${ruleId}-${vpcConfig.name}`, { - resolverRuleId: ruleId, - vpcId: vpcOutput.vpcId, - }); + const roleOutput = IamRoleOutputFinder.tryFindOneByName({ + outputs, + accountKey, + roleKey: 'CentralEndpointDeployment', + }); + if (!roleOutput) { + continue; } + + const ruleIds = [...resolverRegionoutputs.rules?.madRules!, ...resolverRegionoutputs.rules?.onPremRules!]; + new AssociateResolverRules(accountStack, `Rule-Association-${vpcConfig.name}`, { + resolverRuleIds: ruleIds, + roleArn: roleOutput.roleArn, + vpcId: vpcOutput.vpcId, + }); } } diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-4.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-4.ts index 4fb956731..90e495e61 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-4.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-4.ts @@ -6,9 +6,15 @@ import { HostedZoneOutputFinder } from '@aws-accelerator/common-outputs/src/host import { Account, getAccountId } from '../../utils/accounts'; import { AssociateHostedZones } from '@aws-accelerator/custom-resource-associate-hosted-zones'; import * as cdk from '@aws-cdk/core'; -import { StaticResource } from '../../utils/static-resources'; -import { STS } from '@aws-accelerator/common/src/aws/sts'; -import { DynamoDB } from '@aws-accelerator/common/src/aws/dynamodb'; +import { + StaticResourcesOutputFinder, + StaticResourcesOutput, +} from '@aws-accelerator/common-outputs/src/static-resource'; +import { CfnStaticResourcesOutput } from './outputs'; + +// Changing this will result to redeploy most of the stack +const MAX_RESOURCES_IN_STACK = 2; +const RESOURCE_TYPE = 'HostedZoneAssociation'; export interface CentralEndpointsStep4Props { accountStacks: AccountStacks; @@ -17,26 +23,13 @@ export interface CentralEndpointsStep4Props { accounts: Account[]; executionRole: string; assumeRole: string; - staticResources: StaticResource[]; - acceleratorExecutionRoleName: string; - resourcesTableName: string; } /** * Associate VPC to Hosted Zones to Vpcs based on use-central-endpoints */ export async function step4(props: CentralEndpointsStep4Props) { - const { - accountStacks, - config, - outputs, - accounts, - assumeRole, - executionRole, - staticResources, - acceleratorExecutionRoleName, - resourcesTableName, - } = props; + const { accountStacks, config, outputs, accounts, assumeRole, executionRole } = props; const allVpcConfigs = config.getVpcConfigs(); const zonesConfig = config['global-options'].zones; const globalPrivateHostedZoneIds: string[] = []; @@ -58,32 +51,22 @@ export async function step4(props: CentralEndpointsStep4Props) { } } - const masterAccountId = getAccountId(accounts, masterAccountKey)!; - - const sts = new STS(); - const masterAcctCredentials = await sts.getCredentialsForAccountAndRole( - masterAccountId, - acceleratorExecutionRoleName, - ); - - const dynamodb = new DynamoDB(masterAcctCredentials); + const staticResources: StaticResourcesOutput[] = StaticResourcesOutputFinder.findAll({ + outputs, + accountKey: masterAccountKey, + }).filter(sr => sr.resourceType === RESOURCE_TYPE); const existingRegionResources: { [region: string]: string[] } = {}; - const updateStackResources: StaticResource[] = []; const supportedregions = config['global-options']['supported-regions']; const regionalMaxSuffix: { [region: string]: number } = {}; supportedregions.forEach(reg => { - const localSuffix = staticResources - .filter(sr => sr.resourceType === 'HostedZoneAssociation' && sr.region === reg) - .flatMap(r => r.suffix); + const localSuffix = staticResources.filter(sr => sr.region === reg).flatMap(r => r.suffix); regionalMaxSuffix[reg] = localSuffix.length === 0 ? 1 : Math.max(...localSuffix); }); supportedregions.forEach(reg => { - existingRegionResources[reg] = staticResources - .filter(sr => sr.region === reg && sr.resourceType === 'HostedZoneAssociation') - .flatMap(r => r.resources); + existingRegionResources[reg] = staticResources.filter(sr => sr.region === reg).flatMap(r => r.resources); }); for (const { accountKey, vpcConfig } of allVpcConfigs) { @@ -114,29 +97,19 @@ export async function step4(props: CentralEndpointsStep4Props) { } let suffix = regionalMaxSuffix[vpcConfig.region]; - const existingResources = staticResources.find( - sr => - sr.region === vpcConfig.region && - sr.suffix === suffix && - sr.resourceType === `HostedZoneAssociation` && - sr.accountKey === masterAccountKey, - ); - if (existingResources && existingResources.resources.length >= 2) { + const existingResources = staticResources.find(sr => sr.region === vpcConfig.region && sr.suffix === suffix); + + if (existingResources && existingResources.resources.length >= MAX_RESOURCES_IN_STACK) { regionalMaxSuffix[vpcConfig.region] = ++suffix; } let stackSuffix = `HostedZonesAssc-${suffix}`; - let updateDbRequired = true; + let updateOutputsRequired = true; const constructName = `AssociateHostedZones-${accountKey}-${vpcConfig.name}-${vpcConfig.region}`; const phzConstructName = `AssociatePrivateZones-${accountKey}-${vpcConfig.name}-${vpcConfig.region}`; if (existingRegionResources[vpcConfig.region].includes(constructName)) { - updateDbRequired = false; - const regionStacks = staticResources.filter( - sr => - sr.region === vpcConfig.region && - sr.resourceType === 'HostedZoneAssociation' && - sr.accountKey === masterAccountKey, - ); + updateOutputsRequired = false; + const regionStacks = staticResources.filter(sr => sr.region === vpcConfig.region); for (const rs of regionStacks) { if (rs.resources.includes(constructName)) { stackSuffix = `HostedZonesAssc-${rs.suffix}`; @@ -192,96 +165,47 @@ export async function step4(props: CentralEndpointsStep4Props) { } // Update stackResources Object if new resource came - if (updateDbRequired) { + if (updateOutputsRequired) { const currentSuffixIndex = staticResources.findIndex( - sr => - sr.region === vpcConfig.region && - sr.suffix === suffix && - sr.resourceType === 'HostedZoneAssociation' && - sr.accountKey === masterAccountKey, - ); - const localUpdateIndex = updateStackResources.findIndex( - sr => - sr.region === vpcConfig.region && - sr.suffix === suffix && - sr.resourceType === 'HostedZoneAssociation' && - sr.accountKey === masterAccountKey, + sr => sr.region === vpcConfig.region && sr.suffix === suffix, ); const vpcAssociationResources = [constructName]; if (seperateGlobalHostedZonesAccount) { vpcAssociationResources.push(phzConstructName); } if (currentSuffixIndex === -1) { - staticResources.push({ + const currentResourcesObject = { accountKey: masterAccountKey, id: `AssociateHostedZones-${vpcConfig.region}-${masterAccountKey}-${suffix}`, region: vpcConfig.region, - resourceType: 'HostedZoneAssociation', - resources: vpcAssociationResources, + resourceType: RESOURCE_TYPE, + resources: [constructName], suffix, - }); + }; + if (seperateGlobalHostedZonesAccount) { + currentResourcesObject.resources.push(phzConstructName); + } + staticResources.push(currentResourcesObject); } else { const currentResourcesObject = staticResources[currentSuffixIndex]; - currentResourcesObject.resources.push(...vpcAssociationResources); + currentResourcesObject.resources.push(constructName); staticResources[currentSuffixIndex] = currentResourcesObject; } - if (localUpdateIndex === -1) { - updateStackResources.push({ - accountKey: masterAccountKey, - id: `AssociateHostedZones-${vpcConfig.region}-${masterAccountKey}-${suffix}`, - region: vpcConfig.region, - resourceType: 'HostedZoneAssociation', - resources: vpcAssociationResources, - suffix, - }); - } else { - const currentResourcesObject = updateStackResources[currentSuffixIndex]; - currentResourcesObject.resources.push(...vpcAssociationResources); - updateStackResources[currentSuffixIndex] = currentResourcesObject; - } } } - for (const staticResource of updateStackResources) { - const updateExpression = dynamodb.getUpdateValueInput([ - { - key: 'a', - name: 'accountKey', - type: 'S', - value: staticResource.accountKey, - }, - { - key: 'r', - name: 'region', - type: 'S', - value: staticResource.region, - }, - { - key: 'p', - name: 'suffix', - type: 'N', - value: `${staticResource.suffix}`, - }, - { - key: 'res', - name: 'resources', - type: 'S', - value: JSON.stringify(staticResource.resources), - }, - { - key: 'rt', - name: 'resourceType', - type: 'S', - value: staticResource.resourceType, - }, - ]); - await dynamodb.updateItem({ - TableName: resourcesTableName, - Key: { - id: { S: staticResource.id }, - }, - ...updateExpression, - }); + for (const sr of staticResources) { + const accountStack = accountStacks.tryGetOrCreateAccountStack( + sr.accountKey, + sr.region, + `HostedZonesAssc-${sr.suffix}`, + ); + if (!accountStack) { + throw new Error( + `Not able to get or create stack for ${sr.accountKey}: ${sr.region}: HostedZonesAssc-${sr.suffix}`, + ); + } + new CfnStaticResourcesOutput(accountStack, `StaticResourceOutput-${sr.suffix}`, sr); } } diff --git a/src/deployments/cdk/src/deployments/iam/central-endpoints-deployment-roles.ts b/src/deployments/cdk/src/deployments/iam/central-endpoints-deployment-roles.ts new file mode 100644 index 000000000..53cdd8a2b --- /dev/null +++ b/src/deployments/cdk/src/deployments/iam/central-endpoints-deployment-roles.ts @@ -0,0 +1,60 @@ +import * as iam from '@aws-cdk/aws-iam'; +import { AccountStacks, AccountStack } from '../../common/account-stacks'; +import { createIamRoleOutput } from './outputs'; +import { AcceleratorConfig, ResolversConfigType } from '@aws-accelerator/common-config/src'; + +export interface CreateCentralEndpointDeploymentRoleProps { + accountStacks: AccountStacks; + config: AcceleratorConfig; +} + +export async function createCentralEndpointDeploymentRole( + props: CreateCentralEndpointDeploymentRoleProps, +): Promise { + const { accountStacks, config } = props; + const accountRoles: { [accountKey: string]: iam.IRole } = {}; + for (const { accountKey, vpcConfig } of config.getVpcConfigs()) { + if (!vpcConfig['use-central-endpoints'] && !ResolversConfigType.is(vpcConfig.resolvers)) { + continue; + } + if (accountRoles[accountKey]) { + continue; + } + const accountStack = accountStacks.tryGetOrCreateAccountStack(accountKey); + if (!accountStack) { + console.warn(`Cannot find account stack ${accountKey}`); + continue; + } + const centralEndpointRole = await centralEndpointDeploymentRole(accountStack); + accountRoles[accountKey] = centralEndpointRole; + createIamRoleOutput(accountStack, centralEndpointRole, 'CentralEndpointDeployment'); + } +} + +export async function centralEndpointDeploymentRole(stack: AccountStack) { + const role = new iam.Role(stack, 'Custom::CentralEndpointDeployment', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + role.addToPrincipalPolicy( + new iam.PolicyStatement({ + actions: [ + "route53resolver:ListResolverRules", + "ec2:DescribeVpcs", + "route53resolver:DeleteResolverRule", + "route53resolver:AssociateResolverRule", + "route53resolver:ListResolverRuleAssociations", + "route53resolver:CreateResolverRule", + "route53resolver:DisassociateResolverRule"], + resources: ['*'], + }), + ); + + role.addToPrincipalPolicy( + new iam.PolicyStatement({ + actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], + resources: ['*'], + }), + ); + return role; +} diff --git a/src/deployments/cdk/src/deployments/iam/index.ts b/src/deployments/cdk/src/deployments/iam/index.ts index 4c8f36e88..252a4ea6a 100644 --- a/src/deployments/cdk/src/deployments/iam/index.ts +++ b/src/deployments/cdk/src/deployments/iam/index.ts @@ -14,3 +14,4 @@ export * from './tgw-accept-peering-roles'; export * from './logs-metric-filter-role'; export * from './sns-subscriber-lambda-role'; export * from './cleanup-role'; +export * from './central-endpoints-deployment-roles'; diff --git a/src/deployments/cdk/src/utils/context.ts b/src/deployments/cdk/src/utils/context.ts index 85d989386..b8be11b93 100644 --- a/src/deployments/cdk/src/utils/context.ts +++ b/src/deployments/cdk/src/utils/context.ts @@ -15,7 +15,6 @@ export interface Context { acceleratorStateMachineName: string; configRootFilePath: string; installerVersion: string; - resourcesTableName: string; } export function loadContext(): Context { @@ -42,6 +41,5 @@ export function loadContext(): Context { acceleratorStateMachineName: process.env.ACCELERATOR_STATE_MACHINE_NAME!, configRootFilePath: process.env.CONFIG_ROOT_FILE_PATH!, installerVersion: process.env.INSTALLER_VERSION!, - resourcesTableName: process.env.STACK_RESOURCES_TABLE_NAME!, }; } diff --git a/src/deployments/cdk/src/utils/static-resources.ts b/src/deployments/cdk/src/utils/static-resources.ts deleted file mode 100644 index a5c5e21d1..000000000 --- a/src/deployments/cdk/src/utils/static-resources.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { DynamoDB } from '@aws-accelerator/common/src/aws/dynamodb'; - -export interface StaticResource { - id: string; - region: string; - accountKey: string; - suffix: number; - resourceType: string; - resources: string[]; -} - -const dynamodb = new DynamoDB(); - -export async function loadStaticResources(tableName: string): Promise { - const staticResources: StaticResource[] = []; - const staticResourcesResponse = await dynamodb.scan({ - TableName: tableName, - }); - if (!staticResourcesResponse) { - console.warn(`Did not find outputs in DynamoDB table "${tableName}"`); - return staticResources; - } - for (const item of staticResourcesResponse) { - const cVal: StaticResource = { - accountKey: item.accountKey.S!, - id: item.id.S!, - region: item.region.S!, - resourceType: item.resourceType.S!, - resources: JSON.parse(item.resources.S!), - suffix: parseInt(item.suffix.N!, 10), - }; - staticResources.push(cVal); - } - return staticResources; -} diff --git a/src/lib/common-outputs/src/static-resource.ts b/src/lib/common-outputs/src/static-resource.ts new file mode 100644 index 000000000..b5a2d5bd9 --- /dev/null +++ b/src/lib/common-outputs/src/static-resource.ts @@ -0,0 +1,40 @@ +import * as t from 'io-ts'; +import { createStructuredOutputFinder } from './structured-output'; +import { StackOutput } from './stack-output'; +import { enumType, optional } from '@aws-accelerator/common-types'; + +export const RESOURCETYPE = ['PUBLIC', 'PRIVATE'] as const; + +export const ResourceType = enumType(RESOURCETYPE, 'ResourceType'); + +export const StaticResourcesOutput = t.interface( + { + id: t.string, + region: t.string, + accountKey: t.string, + suffix: t.number, + resourceType: t.string, + resources: t.array(t.string), + }, + 'StaticResourcesOutput', +); + +export type StaticResourcesOutput = t.TypeOf; + +export const StaticResourcesOutputFinder = createStructuredOutputFinder(StaticResourcesOutput, finder => ({ + tryFindOneByAccountAndRegionAndType: (props: { + outputs: StackOutput[]; + accountKey?: string; + region?: string; + resourceType?: string; + suffix?: number; + }) => + finder.tryFindOne({ + outputs: props.outputs, + predicate: output => + (props.accountKey === undefined || output.accountKey === props.accountKey) && + (props.region === undefined || output.region === props.region) && + (props.resourceType === undefined || output.resourceType === props.resourceType) && + (props.suffix === undefined || output.suffix === props.suffix), + }), +})); diff --git a/src/lib/custom-resources/cdk-associate-resolver-rules/README.md b/src/lib/custom-resources/cdk-associate-resolver-rules/README.md new file mode 100644 index 000000000..dc91df26a --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-resolver-rules/README.md @@ -0,0 +1,36 @@ +# Security Hub Enable Standards + +This is a custom resource to enable Security Hub Standards and disable specific controls Used `describeStandards`, `batchEnableStandards`, `describeStandardControls` and `updateStandardControls` API calls. + +## Usage + + import { SecurityHubEnable } from '@aws-accelerator/custom-resource-security-hub-enable'; + + const enableSecurityHubResource = new SecurityHubEnable(this, 'EnableSecurityHubStandards`, { + standards: standards.standards, + }); + +## Input Example + + [ + { + "name": "AWS Foundational Security Best Practices v1.0.0", + "controls-to-disable": [ + "IAM.1" + ] + }, + { + "name": "PCI DSS v3.2.1", + "controls-to-disable": [ + "PCI.IAM.3", + "PCIDSS8.3.1" + ] + }, + { + "name": "CIS AWS Foundations Benchmark v1.2.0", + "controls-to-disable": [ + "CIS.1.3", + "CIS1.11" + ] + } + ] diff --git a/src/lib/custom-resources/cdk-associate-resolver-rules/cdk/index.ts b/src/lib/custom-resources/cdk-associate-resolver-rules/cdk/index.ts new file mode 100644 index 000000000..884914604 --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-resolver-rules/cdk/index.ts @@ -0,0 +1,56 @@ +import * as path from 'path'; +import * as cdk from '@aws-cdk/core'; +import * as iam from '@aws-cdk/aws-iam'; +import * as lambda from '@aws-cdk/aws-lambda'; + +const resourceType = 'Custom::AssociateResolverRules'; + +export interface AssociateResolverRuleProps { + vpcId: string; + resolverRuleIds: string[]; + roleArn: string; +} + +export interface AssociateResolverRuleRuntimeProps extends Omit {} +/** + * Custom resource that will create SSM Document. + */ +export class AssociateResolverRules extends cdk.Construct { + private readonly resource: cdk.CustomResource; + private role: iam.IRole; + + constructor(scope: cdk.Construct, id: string, props: AssociateResolverRuleProps) { + super(scope, id); + this.role = iam.Role.fromRoleArn(this, `${resourceType}Role`, props.roleArn); + + const runtimeProps: AssociateResolverRuleRuntimeProps = props; + this.resource = new cdk.CustomResource(this, 'Resource', { + resourceType, + serviceToken: this.lambdaFunction.functionArn, + properties: { + ...runtimeProps, + }, + }); + } + + private get lambdaFunction(): lambda.Function { + const constructName = `${resourceType}Lambda`; + const stack = cdk.Stack.of(this); + const existing = stack.node.tryFindChild(constructName); + if (existing) { + return existing as lambda.Function; + } + + // TODO: Use existing Lambda function to avoid creating multiple Lambda function in same account and region in different stacks + const lambdaPath = require.resolve('@aws-accelerator/custom-resource-associate-resolver-rules-runtime'); + const lambdaDir = path.dirname(lambdaPath); + + return new lambda.Function(stack, constructName, { + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromAsset(lambdaDir), + handler: 'index.handler', + role: this.role, + timeout: cdk.Duration.minutes(15), + }); + } +} diff --git a/src/lib/custom-resources/cdk-associate-resolver-rules/package.json b/src/lib/custom-resources/cdk-associate-resolver-rules/package.json new file mode 100644 index 000000000..4e3c6fb62 --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-resolver-rules/package.json @@ -0,0 +1,24 @@ +{ + "name": "@aws-accelerator/custom-resource-associate-resolver-rules", + "peerDependencies": { + "@aws-cdk/aws-iam": "^1.46.0", + "@aws-cdk/core": "^1.46.0" + }, + "main": "cdk/index.ts", + "private": true, + "version": "0.0.1", + "dependencies": { + "@aws-cdk/aws-iam": "1.46.0", + "@aws-cdk/core": "1.46.0", + "@aws-cdk/aws-lambda": "1.46.0" + }, + "devDependencies": { + "tslint": "6.1.0", + "tslint-config-standard": "9.0.0", + "@types/aws-lambda": "8.10.46", + "tslint-config-prettier": "1.18.0", + "@types/cfn-response": "1.0.3", + "@types/node": "12.12.6", + "@aws-accelerator/custom-resource-associate-resolver-rules-runtime": "workspace:^0.0.1" + } +} \ No newline at end of file diff --git a/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/.gitignore b/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/.gitignore new file mode 100644 index 000000000..1521c8b76 --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/.gitignore @@ -0,0 +1 @@ +dist diff --git a/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/package.json b/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/package.json new file mode 100644 index 000000000..de6897f69 --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/package.json @@ -0,0 +1,31 @@ +{ + "name": "@aws-accelerator/custom-resource-associate-resolver-rules-runtime", + "externals": [ + "aws-lambda", + "aws-sdk" + ], + "private": true, + "source": "src/index.ts", + "version": "0.0.1", + "dependencies": { + "@aws-accelerator/custom-resource-runtime-cfn-response": "workspace:^0.0.1", + "@aws-accelerator/custom-resource-cfn-utils": "workspace:^0.0.1", + "exponential-backoff": "3.0.0", + "aws-sdk": "2.668.0", + "aws-lambda": "1.0.5" + }, + "scripts": { + "prepare": "webpack-cli --config webpack.config.ts" + }, + "devDependencies": { + "ts-loader": "7.0.5", + "typescript": "3.8.3", + "@types/aws-lambda": "8.10.46", + "webpack": "4.42.1", + "@aws-accelerator/custom-resource-runtime-webpack-base": "workspace:^0.0.1", + "@types/node": "12.12.6", + "webpack-cli": "3.3.11" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts" +} \ No newline at end of file diff --git a/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/src/index.ts b/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/src/index.ts new file mode 100644 index 000000000..03040b990 --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/src/index.ts @@ -0,0 +1,120 @@ +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; +import { + CloudFormationCustomResourceEvent, + CloudFormationCustomResourceDeleteEvent, + CloudFormationCustomResourceCreateEvent, + CloudFormationCustomResourceUpdateEvent, +} from 'aws-lambda'; +import { errorHandler } from '@aws-accelerator/custom-resource-runtime-cfn-response'; +import { throttlingBackOff } from '@aws-accelerator/custom-resource-cfn-utils'; + +export interface HandlerProperties { + vpcId: string; + resolverRuleIds: string[]; +} + +const route53Resolver = new AWS.Route53Resolver(); + +export const handler = errorHandler(onEvent); + +async function onEvent(event: CloudFormationCustomResourceEvent) { + console.log(`Associating HostedZones to VPC..`); + console.log(JSON.stringify(event, null, 2)); + + // tslint:disable-next-line: switch-default + switch (event.RequestType) { + case 'Create': + return onCreate(event); + case 'Update': + return onUpdate(event); + case 'Delete': + return onDelete(event); + } +} + +async function onCreate(event: CloudFormationCustomResourceCreateEvent) { + const properties = (event.ResourceProperties as unknown) as HandlerProperties; + const { resolverRuleIds, vpcId } = properties; + for (const ruleId of resolverRuleIds) { + try { + await throttlingBackOff(() => + route53Resolver + .associateResolverRule({ + ResolverRuleId: ruleId, + VPCId: vpcId, + }) + .promise(), + ); + } catch (error) { + console.error(error); + } + } + return { + physicalResourceId: `AssociateResolverRules-${vpcId}`, + }; +} + +async function onUpdate(event: CloudFormationCustomResourceUpdateEvent) { + const properties = (event.ResourceProperties as unknown) as HandlerProperties; + const { resolverRuleIds, vpcId } = properties; + + const oldProperties = (event.OldResourceProperties as unknown) as HandlerProperties; + const newAssociations = resolverRuleIds.filter(rule => !oldProperties.resolverRuleIds.includes(rule)); + const removeAssociations = oldProperties.resolverRuleIds.filter(rule => !resolverRuleIds.includes(rule)); + for (const ruleId of newAssociations) { + try { + await throttlingBackOff(() => + route53Resolver + .associateResolverRule({ + ResolverRuleId: ruleId, + VPCId: vpcId, + }) + .promise(), + ); + } catch (error) { + console.error(error); + } + } + + for (const ruleId of removeAssociations) { + try { + await throttlingBackOff(() => + route53Resolver + .disassociateResolverRule({ + ResolverRuleId: ruleId, + VPCId: vpcId, + }) + .promise(), + ); + } catch (error) { + console.error(error); + } + } + + return { + physicalResourceId: `AssociateResolverRules-${vpcId}`, + }; +} + +async function onDelete(event: CloudFormationCustomResourceDeleteEvent) { + console.log(`Deleting Log Group Metric filter...`); + console.log(JSON.stringify(event, null, 2)); + const properties = (event.ResourceProperties as unknown) as HandlerProperties; + const { resolverRuleIds, vpcId } = properties; + + for (const ruleId of resolverRuleIds) { + try { + await throttlingBackOff(() => + route53Resolver + .disassociateResolverRule({ + ResolverRuleId: ruleId, + VPCId: vpcId, + }) + .promise(), + ); + } catch (error) { + console.error(error); + } + } +} diff --git a/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/tsconfig.json b/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/tsconfig.json new file mode 100644 index 000000000..118a8376a --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es2019", + "lib": ["es2019"], + "module": "commonjs", + "moduleResolution": "node", + "strict": true, + "declaration": true, + "esModuleInterop": true, + "noImplicitAny": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.spec.ts"] +} diff --git a/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/webpack.config.ts b/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/webpack.config.ts new file mode 100644 index 000000000..425acd8ba --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/webpack.config.ts @@ -0,0 +1,4 @@ +import { webpackConfigurationForPackage } from '@aws-accelerator/custom-resource-runtime-webpack-base'; +import pkg from './package.json'; + +export default webpackConfigurationForPackage(pkg); diff --git a/src/lib/custom-resources/cdk-associate-resolver-rules/tsconfig.json b/src/lib/custom-resources/cdk-associate-resolver-rules/tsconfig.json new file mode 100644 index 000000000..4db940b9b --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-resolver-rules/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "es2019", + "lib": [ + "es2019" + ], + "noImplicitAny": true, + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "moduleResolution": "node", + "outDir": "dist" + }, + "exclude": [ + "node_modules", + "**/*.spec.ts" + ], + "include": [ + "cdk/**/*" + ] +} \ No newline at end of file diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/README.md b/src/lib/custom-resources/cdk-create-resolver-rule/README.md new file mode 100644 index 000000000..dc91df26a --- /dev/null +++ b/src/lib/custom-resources/cdk-create-resolver-rule/README.md @@ -0,0 +1,36 @@ +# Security Hub Enable Standards + +This is a custom resource to enable Security Hub Standards and disable specific controls Used `describeStandards`, `batchEnableStandards`, `describeStandardControls` and `updateStandardControls` API calls. + +## Usage + + import { SecurityHubEnable } from '@aws-accelerator/custom-resource-security-hub-enable'; + + const enableSecurityHubResource = new SecurityHubEnable(this, 'EnableSecurityHubStandards`, { + standards: standards.standards, + }); + +## Input Example + + [ + { + "name": "AWS Foundational Security Best Practices v1.0.0", + "controls-to-disable": [ + "IAM.1" + ] + }, + { + "name": "PCI DSS v3.2.1", + "controls-to-disable": [ + "PCI.IAM.3", + "PCIDSS8.3.1" + ] + }, + { + "name": "CIS AWS Foundations Benchmark v1.2.0", + "controls-to-disable": [ + "CIS.1.3", + "CIS1.11" + ] + } + ] diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/cdk/index.ts b/src/lib/custom-resources/cdk-create-resolver-rule/cdk/index.ts new file mode 100644 index 000000000..86306ed98 --- /dev/null +++ b/src/lib/custom-resources/cdk-create-resolver-rule/cdk/index.ts @@ -0,0 +1,63 @@ +import * as path from 'path'; +import * as cdk from '@aws-cdk/core'; +import * as iam from '@aws-cdk/aws-iam'; +import * as lambda from '@aws-cdk/aws-lambda'; + +const resourceType = 'Custom::CreateResolverRule'; + +export interface CreateResolverRuleProps { + vpcId: string; + domainName: string; + targetIps: string[]; + resolverEndpointId: string; + name: string; + roleArn: string; +} + +export interface CreateResolverRuleRuntimeProps extends Omit {} +/** + * Custom resource that will create Resolver Rule. + */ +export class CreateResolverRule extends cdk.Construct { + private readonly resource: cdk.CustomResource; + private role: iam.IRole; + + constructor(scope: cdk.Construct, id: string, props: CreateResolverRuleProps) { + super(scope, id); + this.role = iam.Role.fromRoleArn(this, `${resourceType}Role`, props.roleArn); + + const runtimeProps: CreateResolverRuleRuntimeProps = props; + this.resource = new cdk.CustomResource(this, 'Resource', { + resourceType, + serviceToken: this.lambdaFunction.functionArn, + properties: { + ...runtimeProps, + }, + }); + } + + get ruleId(): string { + return this.resource.getAttString('RuleId'); + } + + private get lambdaFunction(): lambda.Function { + const constructName = `${resourceType}Lambda`; + const stack = cdk.Stack.of(this); + const existing = stack.node.tryFindChild(constructName); + if (existing) { + return existing as lambda.Function; + } + + // TODO: Use existing Lambda function to avoid creating multiple Lambda function in same account and region in different stacks + const lambdaPath = require.resolve('@aws-accelerator/custom-resource-create-resolver-rule-runtime'); + const lambdaDir = path.dirname(lambdaPath); + + return new lambda.Function(stack, constructName, { + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromAsset(lambdaDir), + handler: 'index.handler', + role: this.role, + timeout: cdk.Duration.minutes(15), + }); + } +} diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/package.json b/src/lib/custom-resources/cdk-create-resolver-rule/package.json new file mode 100644 index 000000000..d642fa0e9 --- /dev/null +++ b/src/lib/custom-resources/cdk-create-resolver-rule/package.json @@ -0,0 +1,24 @@ +{ + "name": "@aws-accelerator/custom-resource-create-resolver-rule", + "peerDependencies": { + "@aws-cdk/aws-iam": "^1.46.0", + "@aws-cdk/core": "^1.46.0" + }, + "main": "cdk/index.ts", + "private": true, + "version": "0.0.1", + "dependencies": { + "@aws-cdk/aws-iam": "1.46.0", + "@aws-cdk/core": "1.46.0", + "@aws-cdk/aws-lambda": "1.46.0" + }, + "devDependencies": { + "tslint": "6.1.0", + "tslint-config-standard": "9.0.0", + "@types/aws-lambda": "8.10.46", + "tslint-config-prettier": "1.18.0", + "@types/cfn-response": "1.0.3", + "@types/node": "12.12.6", + "@aws-accelerator/custom-resource-create-resolver-rule-runtime": "workspace:^0.0.1" + } +} \ No newline at end of file diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/.gitignore b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/.gitignore new file mode 100644 index 000000000..1521c8b76 --- /dev/null +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/.gitignore @@ -0,0 +1 @@ +dist diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/package.json b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/package.json new file mode 100644 index 000000000..62303c5be --- /dev/null +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/package.json @@ -0,0 +1,31 @@ +{ + "name": "@aws-accelerator/custom-resource-create-resolver-rule-runtime", + "externals": [ + "aws-lambda", + "aws-sdk" + ], + "private": true, + "source": "src/index.ts", + "version": "0.0.1", + "dependencies": { + "@aws-accelerator/custom-resource-runtime-cfn-response": "workspace:^0.0.1", + "@aws-accelerator/custom-resource-cfn-utils": "workspace:^0.0.1", + "exponential-backoff": "3.0.0", + "aws-sdk": "2.668.0", + "aws-lambda": "1.0.5" + }, + "scripts": { + "prepare": "webpack-cli --config webpack.config.ts" + }, + "devDependencies": { + "ts-loader": "7.0.5", + "typescript": "3.8.3", + "@types/aws-lambda": "8.10.46", + "webpack": "4.42.1", + "@aws-accelerator/custom-resource-runtime-webpack-base": "workspace:^0.0.1", + "@types/node": "12.12.6", + "webpack-cli": "3.3.11" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts" +} \ No newline at end of file diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts new file mode 100644 index 000000000..328ac51fb --- /dev/null +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts @@ -0,0 +1,138 @@ +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; +import { CloudFormationCustomResourceEvent, CloudFormationCustomResourceDeleteEvent } from 'aws-lambda'; +import { errorHandler } from '@aws-accelerator/custom-resource-runtime-cfn-response'; +import { delay, throttlingBackOff } from '@aws-accelerator/custom-resource-cfn-utils'; + +export interface HandlerProperties { + vpcId: string; + domainName: string; + targetIps: string[]; + resolverEndpointId: string; + name: string; +} + +const route53Resolver = new AWS.Route53Resolver(); + +export const handler = errorHandler(onEvent); + +async function onEvent(event: CloudFormationCustomResourceEvent) { + console.log(`Create Resolver Rule..`); + console.log(JSON.stringify(event, null, 2)); + + // tslint:disable-next-line: switch-default + switch (event.RequestType) { + case 'Create': + return onCreateOrUpdate(event); + case 'Update': + return onCreateOrUpdate(event); + case 'Delete': + return onDelete(event); + } +} + +async function onCreateOrUpdate(event: CloudFormationCustomResourceEvent) { + const properties = (event.ResourceProperties as unknown) as HandlerProperties; + const { targetIps, vpcId, domainName, resolverEndpointId, name } = properties; + const targetIpParams: AWS.Route53Resolver.TargetAddress[] = []; + targetIps.forEach(ip => { + targetIpParams.push({ + Ip: ip, + }); + }); + let resolverRuleId: string; + try { + const ruleResponse = await throttlingBackOff(() => + route53Resolver + .createResolverRule({ + DomainName: domainName, + CreatorRequestId: `${resolverEndpointId}-${domainName}`, + RuleType: 'FORWARD', + ResolverEndpointId: resolverEndpointId, + TargetIps: targetIpParams, + Name: name + }) + .promise(), + ); + resolverRuleId = ruleResponse.ResolverRule?.Id!; + } catch (error) { + throw new Error(error); + } + + try { + const ruleAssociationResponse = await throttlingBackOff(() => + route53Resolver + .associateResolverRule({ + ResolverRuleId: resolverRuleId, + VPCId: vpcId, + }) + .promise(), + ); + } catch (error) { + console.log(error); + } + + return { + physicalResourceId: `CreateResolverRule-${resolverRuleId!}`, + data: { + RuleId: resolverRuleId, + } + }; +} + +async function onDelete(event: CloudFormationCustomResourceDeleteEvent) { + console.log(`Deleting Resolver Rule...`); + console.log(JSON.stringify(event, null, 2)); + const properties = (event.ResourceProperties as unknown) as HandlerProperties; + const { resolverEndpointId, name } = properties; + let maxRetries = 20; + const resolverRule = await throttlingBackOff(() => route53Resolver.listResolverRules({ + Filters: [{ + Name: 'ResolverEndpointId', + Values: [resolverEndpointId] + }, + { + Name: 'Name', + Values: [name] + }] + }).promise()); + for (const rule of resolverRule.ResolverRules! || []) { + let associatedVpcs = await getVpcIds(rule.Id!); + + for (const vpcId of associatedVpcs! || []) { + await throttlingBackOff(() => route53Resolver.disassociateResolverRule({ + ResolverRuleId: rule.Id!, + VPCId: vpcId!, + }).promise()); + } + + do { + associatedVpcs = await getVpcIds(rule.Id!); + // Waiting to disassociate VPC Ids from the resolver rule + await delay(5000); + } while ((associatedVpcs || []).length > 0 && maxRetries-- > 0); + + await throttlingBackOff(() => route53Resolver.deleteResolverRule({ + ResolverRuleId: rule.Id!, + }).promise()); + } +} + +async function getVpcIds(resolverRuleId: string) { + // Get the vpc associations for the resolver + const associations = await throttlingBackOff(() => + route53Resolver + .listResolverRuleAssociations({ + Filters: [ + { + Name: 'ResolverRuleId', + Values: [resolverRuleId], + }, + ], + }) + .promise(), + ); + + const vpcIds = associations.ResolverRuleAssociations?.map(a => a.VPCId); + return vpcIds; +} diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/tsconfig.json b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/tsconfig.json new file mode 100644 index 000000000..118a8376a --- /dev/null +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es2019", + "lib": ["es2019"], + "module": "commonjs", + "moduleResolution": "node", + "strict": true, + "declaration": true, + "esModuleInterop": true, + "noImplicitAny": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.spec.ts"] +} diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/webpack.config.ts b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/webpack.config.ts new file mode 100644 index 000000000..425acd8ba --- /dev/null +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/webpack.config.ts @@ -0,0 +1,4 @@ +import { webpackConfigurationForPackage } from '@aws-accelerator/custom-resource-runtime-webpack-base'; +import pkg from './package.json'; + +export default webpackConfigurationForPackage(pkg); diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/tsconfig.json b/src/lib/custom-resources/cdk-create-resolver-rule/tsconfig.json new file mode 100644 index 000000000..4db940b9b --- /dev/null +++ b/src/lib/custom-resources/cdk-create-resolver-rule/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "es2019", + "lib": [ + "es2019" + ], + "noImplicitAny": true, + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "moduleResolution": "node", + "outDir": "dist" + }, + "exclude": [ + "node_modules", + "**/*.spec.ts" + ], + "include": [ + "cdk/**/*" + ] +} \ No newline at end of file From 3a010dd299bf25dbbde27de21d50f32dc0d94049 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Wed, 16 Sep 2020 21:19:39 +0530 Subject: [PATCH 05/25] Prettier --- .../deployments/central-endpoints/step-2.ts | 52 ++++++++++++------- .../deployments/central-endpoints/step-3.ts | 28 ++++++---- .../iam/central-endpoints-deployment-roles.ts | 15 +++--- .../runtime/src/index.ts | 52 ++++++++++++------- 4 files changed, 90 insertions(+), 57 deletions(-) diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts index f4c131857..d1364afb0 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts @@ -87,8 +87,12 @@ export async function step2(props: CentralEndpointsStep2Props) { } let stackSuffix: string; - if (!!zonesConfig.find(zc => zc.account === accountKey && zc["resolver-vpc"] === vpcConfig.name && zc.region === vpcConfig.region)) { - stackSuffix= `EndpointsRules-${vpcConfig.name}`; + if ( + !!zonesConfig.find( + zc => zc.account === accountKey && zc['resolver-vpc'] === vpcConfig.name && zc.region === vpcConfig.region, + ) + ) { + stackSuffix = `EndpointsRules-${vpcConfig.name}`; } else { if (accountRulesCounter[`${accountKey}-${vpcConfig.region}`]) { accountRulesCounter[`${accountKey}-${vpcConfig.region}`] = ++accountRulesCounter[ @@ -98,7 +102,7 @@ export async function step2(props: CentralEndpointsStep2Props) { accountRulesCounter[`${accountKey}-${vpcConfig.region}`] = 1; } // Includes max of 10 VPCs, since we need max 8 resources for one VPC - stackSuffix= `EndpointsRules-${Math.ceil(accountRulesCounter[`${accountKey}-${vpcConfig.region}`] / 10)}`; + stackSuffix = `EndpointsRules-${Math.ceil(accountRulesCounter[`${accountKey}-${vpcConfig.region}`] / 10)}`; } const accountStack = accountStacks.tryGetOrCreateAccountStack(accountKey, vpcConfig.region, stackSuffix); @@ -136,14 +140,18 @@ export async function step2(props: CentralEndpointsStep2Props) { // For each on-premise domain defined in the parameters file, create a Resolver rule which points to the specified IP's for (const onPremRuleConfig of vpcConfig['on-premise-rules'] || []) { - const rule = new CreateResolverRule(accountStack, `${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}-on-prem-phz-rule`, { - domainName: onPremRuleConfig.zone, - resolverEndpointId: r53ResolverEndpoints.outboundEndpointRef!, - roleArn: roleOutput.roleArn, - targetIps: onPremRuleConfig['outbound-ips'], - vpcId: vpcOutput.vpcId, - name: `${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}-on-prem-phz-rule`, - }); + const rule = new CreateResolverRule( + accountStack, + `${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}-on-prem-phz-rule`, + { + domainName: onPremRuleConfig.zone, + resolverEndpointId: r53ResolverEndpoints.outboundEndpointRef!, + roleArn: roleOutput.roleArn, + targetIps: onPremRuleConfig['outbound-ips'], + vpcId: vpcOutput.vpcId, + name: `${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}-on-prem-phz-rule`, + }, + ); rule.node.addDependency(r53ResolverEndpoints); onPremRules.push(rule.ruleId); } @@ -167,15 +175,19 @@ export async function step2(props: CentralEndpointsStep2Props) { continue; } madIPs = madOutput[0].dnsIps.split(','); - - const madRule = new CreateResolverRule(accountStack, `${domainToName(mad['dns-domain'])}-${vpcConfig.name}-phz-rule`, { - domainName: mad['dns-domain'], - resolverEndpointId: r53ResolverEndpoints.outboundEndpointRef!, - roleArn: roleOutput.roleArn, - targetIps: madIPs, - vpcId: vpcOutput.vpcId, - name: `${domainToName(mad['dns-domain'])}-${vpcConfig.name}-mad-phz-rule`, - }); + + const madRule = new CreateResolverRule( + accountStack, + `${domainToName(mad['dns-domain'])}-${vpcConfig.name}-phz-rule`, + { + domainName: mad['dns-domain'], + resolverEndpointId: r53ResolverEndpoints.outboundEndpointRef!, + roleArn: roleOutput.roleArn, + targetIps: madIPs, + vpcId: vpcOutput.vpcId, + name: `${domainToName(mad['dns-domain'])}-${vpcConfig.name}-mad-phz-rule`, + }, + ); madRule.node.addDependency(r53ResolverEndpoints.outboundEndpoint!); madRules.push(madRule.ruleId); } diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-3.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-3.ts index 0ab13d992..c0e2a2b65 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-3.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-3.ts @@ -5,8 +5,10 @@ import * as c from '@aws-accelerator/common-config'; import * as route53resolver from '@aws-cdk/aws-route53resolver'; import { IamRoleOutputFinder } from '@aws-accelerator/common-outputs/src/iam-role'; import { VpcOutputFinder } from '@aws-accelerator/common-outputs/src/vpc'; -import { StaticResourcesOutput, StaticResourcesOutputFinder } from '@aws-accelerator/common-outputs/src/static-resource'; - +import { + StaticResourcesOutput, + StaticResourcesOutputFinder, +} from '@aws-accelerator/common-outputs/src/static-resource'; // Changing this will result to redeploy most of the stack const MAX_RESOURCES_IN_STACK = 2; @@ -26,18 +28,22 @@ export async function step3(props: CentralEndpointsStep3Props) { const allVpcConfigs = config.getVpcConfigs(); const accountRulesCounter: { [accountKey: string]: number } = {}; const supportedregions = config['global-options']['supported-regions']; - + const allStaticResources: StaticResourcesOutput[] = StaticResourcesOutputFinder.findAll({ outputs, }).filter(sr => sr.resourceType === RESOURCE_TYPE); - - const accountStaticResourcesConfig: { [accountKey: string]: StaticResourcesOutput[]} = {}; - const accountRegionExistingResources: { [accountKey: string]: { - [region: string]: string[] - }} = {}; - const accountRegionMaxSuffix: { [accountKey: string]: { - [region: string]: number - }} = {}; + + const accountStaticResourcesConfig: { [accountKey: string]: StaticResourcesOutput[] } = {}; + const accountRegionExistingResources: { + [accountKey: string]: { + [region: string]: string[]; + }; + } = {}; + const accountRegionMaxSuffix: { + [accountKey: string]: { + [region: string]: number; + }; + } = {}; for (const { accountKey, vpcConfig } of allVpcConfigs) { const centralPhzConfig = config['global-options'].zones.find(zc => zc.region === vpcConfig.region); diff --git a/src/deployments/cdk/src/deployments/iam/central-endpoints-deployment-roles.ts b/src/deployments/cdk/src/deployments/iam/central-endpoints-deployment-roles.ts index 53cdd8a2b..b2bff03c4 100644 --- a/src/deployments/cdk/src/deployments/iam/central-endpoints-deployment-roles.ts +++ b/src/deployments/cdk/src/deployments/iam/central-endpoints-deployment-roles.ts @@ -39,13 +39,14 @@ export async function centralEndpointDeploymentRole(stack: AccountStack) { role.addToPrincipalPolicy( new iam.PolicyStatement({ actions: [ - "route53resolver:ListResolverRules", - "ec2:DescribeVpcs", - "route53resolver:DeleteResolverRule", - "route53resolver:AssociateResolverRule", - "route53resolver:ListResolverRuleAssociations", - "route53resolver:CreateResolverRule", - "route53resolver:DisassociateResolverRule"], + 'route53resolver:ListResolverRules', + 'ec2:DescribeVpcs', + 'route53resolver:DeleteResolverRule', + 'route53resolver:AssociateResolverRule', + 'route53resolver:ListResolverRuleAssociations', + 'route53resolver:CreateResolverRule', + 'route53resolver:DisassociateResolverRule', + ], resources: ['*'], }), ); diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts index 328ac51fb..ed53e6d1b 100644 --- a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts @@ -50,7 +50,7 @@ async function onCreateOrUpdate(event: CloudFormationCustomResourceEvent) { RuleType: 'FORWARD', ResolverEndpointId: resolverEndpointId, TargetIps: targetIpParams, - Name: name + Name: name, }) .promise(), ); @@ -76,7 +76,7 @@ async function onCreateOrUpdate(event: CloudFormationCustomResourceEvent) { physicalResourceId: `CreateResolverRule-${resolverRuleId!}`, data: { RuleId: resolverRuleId, - } + }, }; } @@ -86,24 +86,34 @@ async function onDelete(event: CloudFormationCustomResourceDeleteEvent) { const properties = (event.ResourceProperties as unknown) as HandlerProperties; const { resolverEndpointId, name } = properties; let maxRetries = 20; - const resolverRule = await throttlingBackOff(() => route53Resolver.listResolverRules({ - Filters: [{ - Name: 'ResolverEndpointId', - Values: [resolverEndpointId] - }, - { - Name: 'Name', - Values: [name] - }] - }).promise()); + const resolverRule = await throttlingBackOff(() => + route53Resolver + .listResolverRules({ + Filters: [ + { + Name: 'ResolverEndpointId', + Values: [resolverEndpointId], + }, + { + Name: 'Name', + Values: [name], + }, + ], + }) + .promise(), + ); for (const rule of resolverRule.ResolverRules! || []) { let associatedVpcs = await getVpcIds(rule.Id!); for (const vpcId of associatedVpcs! || []) { - await throttlingBackOff(() => route53Resolver.disassociateResolverRule({ - ResolverRuleId: rule.Id!, - VPCId: vpcId!, - }).promise()); + await throttlingBackOff(() => + route53Resolver + .disassociateResolverRule({ + ResolverRuleId: rule.Id!, + VPCId: vpcId!, + }) + .promise(), + ); } do { @@ -112,9 +122,13 @@ async function onDelete(event: CloudFormationCustomResourceDeleteEvent) { await delay(5000); } while ((associatedVpcs || []).length > 0 && maxRetries-- > 0); - await throttlingBackOff(() => route53Resolver.deleteResolverRule({ - ResolverRuleId: rule.Id!, - }).promise()); + await throttlingBackOff(() => + route53Resolver + .deleteResolverRule({ + ResolverRuleId: rule.Id!, + }) + .promise(), + ); } } From de3aea87902ea36a3f10e62ed07ca3716626d0bf Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Wed, 16 Sep 2020 22:39:12 +0530 Subject: [PATCH 06/25] Generating rule name --- .../deployments/central-endpoints/step-2.ts | 21 ++++++++++++++----- .../src/core/accelerator-name-generator.ts | 2 +- .../runtime/src/index.ts | 2 +- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts index d1364afb0..647253ad8 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts @@ -12,7 +12,7 @@ import { ResolverEndpoint, ResolverRule } from '@aws-accelerator/cdk-constructs/ import { JsonOutputValue } from '../../common/json-output'; import { Account, getAccountId } from '../../utils/accounts'; import * as ram from '@aws-cdk/aws-ram'; -import { createName } from '@aws-accelerator/cdk-accelerator/src/core/accelerator-name-generator'; +import { createName, hashPath } from '@aws-accelerator/cdk-accelerator/src/core/accelerator-name-generator'; import { CreateResolverRule } from '@aws-accelerator/custom-resource-create-resolver-rule'; import { IamRoleOutputFinder } from '@aws-accelerator/common-outputs/src/iam-role'; @@ -142,14 +142,14 @@ export async function step2(props: CentralEndpointsStep2Props) { for (const onPremRuleConfig of vpcConfig['on-premise-rules'] || []) { const rule = new CreateResolverRule( accountStack, - `${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}-on-prem-phz-rule`, + `${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}`, { domainName: onPremRuleConfig.zone, resolverEndpointId: r53ResolverEndpoints.outboundEndpointRef!, roleArn: roleOutput.roleArn, targetIps: onPremRuleConfig['outbound-ips'], vpcId: vpcOutput.vpcId, - name: `${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}-on-prem-phz-rule`, + name: createRuleName(`${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}`), }, ); rule.node.addDependency(r53ResolverEndpoints); @@ -178,14 +178,14 @@ export async function step2(props: CentralEndpointsStep2Props) { const madRule = new CreateResolverRule( accountStack, - `${domainToName(mad['dns-domain'])}-${vpcConfig.name}-phz-rule`, + `${domainToName(mad['dns-domain'])}-${vpcConfig.name}`, { domainName: mad['dns-domain'], resolverEndpointId: r53ResolverEndpoints.outboundEndpointRef!, roleArn: roleOutput.roleArn, targetIps: madIPs, vpcId: vpcOutput.vpcId, - name: `${domainToName(mad['dns-domain'])}-${vpcConfig.name}-mad-phz-rule`, + name: createRuleName(`${domainToName(mad['dns-domain'])}-${vpcConfig.name}`), }, ); madRule.node.addDependency(r53ResolverEndpoints.outboundEndpoint!); @@ -248,3 +248,14 @@ export async function step2(props: CentralEndpointsStep2Props) { function domainToName(domain: string): string { return domain.replace(/\./gi, '-'); } + +export function createRuleName(name: string): string { + const hash = hashPath([name], 8); + if (name.length > 44) { + name = name.substring(0, 44) + hash; + } + return createName({ + name, + }); +} + diff --git a/src/lib/cdk-accelerator/src/core/accelerator-name-generator.ts b/src/lib/cdk-accelerator/src/core/accelerator-name-generator.ts index 5acdcf0d8..bd346f7fc 100644 --- a/src/lib/cdk-accelerator/src/core/accelerator-name-generator.ts +++ b/src/lib/cdk-accelerator/src/core/accelerator-name-generator.ts @@ -126,7 +126,7 @@ export function createName(props: CreateNameProps = {}): string { * * https://github.com/aws/aws-cdk/blob/f8df4e04f6f9631f963353903e020cfa8377e8bc/packages/%40aws-cdk/core/lib/private/uniqueid.ts#L33 */ -function hashPath(path: string[], length: number) { +export function hashPath(path: string[], length: number) { const hash = crypto.createHash('md5').update(path.join('/')).digest('hex'); return hash.slice(0, length).toUpperCase(); } diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts index ed53e6d1b..a2484a12c 100644 --- a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts @@ -60,7 +60,7 @@ async function onCreateOrUpdate(event: CloudFormationCustomResourceEvent) { } try { - const ruleAssociationResponse = await throttlingBackOff(() => + await throttlingBackOff(() => route53Resolver .associateResolverRule({ ResolverRuleId: resolverRuleId, From a61ddf7a962fff8252d7fbfc8cbb8fad0d697079 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Wed, 16 Sep 2020 22:39:24 +0530 Subject: [PATCH 07/25] prettier --- .../deployments/central-endpoints/step-2.ts | 41 ++++++++----------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts index 647253ad8..2523b56a4 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts @@ -140,18 +140,14 @@ export async function step2(props: CentralEndpointsStep2Props) { // For each on-premise domain defined in the parameters file, create a Resolver rule which points to the specified IP's for (const onPremRuleConfig of vpcConfig['on-premise-rules'] || []) { - const rule = new CreateResolverRule( - accountStack, - `${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}`, - { - domainName: onPremRuleConfig.zone, - resolverEndpointId: r53ResolverEndpoints.outboundEndpointRef!, - roleArn: roleOutput.roleArn, - targetIps: onPremRuleConfig['outbound-ips'], - vpcId: vpcOutput.vpcId, - name: createRuleName(`${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}`), - }, - ); + const rule = new CreateResolverRule(accountStack, `${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}`, { + domainName: onPremRuleConfig.zone, + resolverEndpointId: r53ResolverEndpoints.outboundEndpointRef!, + roleArn: roleOutput.roleArn, + targetIps: onPremRuleConfig['outbound-ips'], + vpcId: vpcOutput.vpcId, + name: createRuleName(`${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}`), + }); rule.node.addDependency(r53ResolverEndpoints); onPremRules.push(rule.ruleId); } @@ -176,18 +172,14 @@ export async function step2(props: CentralEndpointsStep2Props) { } madIPs = madOutput[0].dnsIps.split(','); - const madRule = new CreateResolverRule( - accountStack, - `${domainToName(mad['dns-domain'])}-${vpcConfig.name}`, - { - domainName: mad['dns-domain'], - resolverEndpointId: r53ResolverEndpoints.outboundEndpointRef!, - roleArn: roleOutput.roleArn, - targetIps: madIPs, - vpcId: vpcOutput.vpcId, - name: createRuleName(`${domainToName(mad['dns-domain'])}-${vpcConfig.name}`), - }, - ); + const madRule = new CreateResolverRule(accountStack, `${domainToName(mad['dns-domain'])}-${vpcConfig.name}`, { + domainName: mad['dns-domain'], + resolverEndpointId: r53ResolverEndpoints.outboundEndpointRef!, + roleArn: roleOutput.roleArn, + targetIps: madIPs, + vpcId: vpcOutput.vpcId, + name: createRuleName(`${domainToName(mad['dns-domain'])}-${vpcConfig.name}`), + }); madRule.node.addDependency(r53ResolverEndpoints.outboundEndpoint!); madRules.push(madRule.ruleId); } @@ -258,4 +250,3 @@ export function createRuleName(name: string): string { name, }); } - From 1d6adcaba28e6f8e9782255bd7cdb100759f42f8 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Wed, 16 Sep 2020 22:57:06 +0530 Subject: [PATCH 08/25] using name as creatorrequestid --- .../cdk-create-resolver-rule/runtime/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts index a2484a12c..475e54924 100644 --- a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts @@ -46,7 +46,7 @@ async function onCreateOrUpdate(event: CloudFormationCustomResourceEvent) { route53Resolver .createResolverRule({ DomainName: domainName, - CreatorRequestId: `${resolverEndpointId}-${domainName}`, + CreatorRequestId: name, RuleType: 'FORWARD', ResolverEndpointId: resolverEndpointId, TargetIps: targetIpParams, From 710aa4bb4d9f9910389b39d93b1ff607addcbd15 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Wed, 16 Sep 2020 23:27:02 +0530 Subject: [PATCH 09/25] adding proper dependency --- src/deployments/cdk/src/deployments/central-endpoints/step-2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts index 2523b56a4..19cd1e9ff 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts @@ -148,7 +148,7 @@ export async function step2(props: CentralEndpointsStep2Props) { vpcId: vpcOutput.vpcId, name: createRuleName(`${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}`), }); - rule.node.addDependency(r53ResolverEndpoints); + rule.node.addDependency(r53ResolverEndpoints.outboundEndpoint!); onPremRules.push(rule.ruleId); } resolverRulesOutput.onPremRules = onPremRules; From e1a2b8babd95bd6b43b489608f0bbc75b4418940 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Thu, 17 Sep 2020 10:37:14 +0530 Subject: [PATCH 10/25] Changing name formati --- .../cdk/src/deployments/central-endpoints/step-2.ts | 4 ++-- .../cdk-create-resolver-rule/runtime/src/index.ts | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts index 19cd1e9ff..6aedcbb91 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts @@ -146,7 +146,7 @@ export async function step2(props: CentralEndpointsStep2Props) { roleArn: roleOutput.roleArn, targetIps: onPremRuleConfig['outbound-ips'], vpcId: vpcOutput.vpcId, - name: createRuleName(`${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}`), + name: createRuleName(`${vpcConfig.name}-onprem-${domainToName(onPremRuleConfig.zone)}`), }); rule.node.addDependency(r53ResolverEndpoints.outboundEndpoint!); onPremRules.push(rule.ruleId); @@ -178,7 +178,7 @@ export async function step2(props: CentralEndpointsStep2Props) { roleArn: roleOutput.roleArn, targetIps: madIPs, vpcId: vpcOutput.vpcId, - name: createRuleName(`${domainToName(mad['dns-domain'])}-${vpcConfig.name}`), + name: createRuleName(`${vpcConfig.name}-mad-${domainToName(mad['dns-domain'])}`), }); madRule.node.addDependency(r53ResolverEndpoints.outboundEndpoint!); madRules.push(madRule.ruleId); diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts index 475e54924..f64276d18 100644 --- a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts @@ -56,6 +56,7 @@ async function onCreateOrUpdate(event: CloudFormationCustomResourceEvent) { ); resolverRuleId = ruleResponse.ResolverRule?.Id!; } catch (error) { + // TODO: Handle Errors throw new Error(error); } @@ -69,6 +70,7 @@ async function onCreateOrUpdate(event: CloudFormationCustomResourceEvent) { .promise(), ); } catch (error) { + // TODO: Handle Errors console.log(error); } @@ -149,4 +151,4 @@ async function getVpcIds(resolverRuleId: string) { const vpcIds = associations.ResolverRuleAssociations?.map(a => a.VPCId); return vpcIds; -} +} \ No newline at end of file From cfc65b5a89f53637891cb4bb7044dc27a7c4eac8 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Thu, 17 Sep 2020 11:29:07 +0530 Subject: [PATCH 11/25] Initiating previous stacks for removing if there are no resources --- .../cdk/src/deployments/central-endpoints/step-4.ts | 5 +++++ src/lib/cdk-constructs/src/route53/resolver-endpoint.ts | 2 ++ .../cdk-create-resolver-rule/runtime/src/index.ts | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-4.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-4.ts index 90e495e61..b1cd36272 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-4.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-4.ts @@ -56,6 +56,11 @@ export async function step4(props: CentralEndpointsStep4Props) { accountKey: masterAccountKey, }).filter(sr => sr.resourceType === RESOURCE_TYPE); + // Initiate previous stacks to handle deletion of previously deployed stack if there are no resources + for (const sr of staticResources) { + accountStacks.tryGetOrCreateAccountStack(sr.accountKey, sr.region, `HostedZonesAssc-${sr.suffix}`); + } + const existingRegionResources: { [region: string]: string[] } = {}; const supportedregions = config['global-options']['supported-regions']; diff --git a/src/lib/cdk-constructs/src/route53/resolver-endpoint.ts b/src/lib/cdk-constructs/src/route53/resolver-endpoint.ts index c72b414b4..099e7a166 100644 --- a/src/lib/cdk-constructs/src/route53/resolver-endpoint.ts +++ b/src/lib/cdk-constructs/src/route53/resolver-endpoint.ts @@ -53,6 +53,7 @@ export class ResolverEndpoint extends cdk.Construct { securityGroupIds: [securityGroup.ref], name: `${this.props.name} Inbound Endpoint`, }); + this._inboundEndpoint.addDependsOn(securityGroup); // const dnsIps = new R53DnsEndpointIps(this, 'InboundIp', { // resolverEndpointId: this._inboundEndpoint.ref, @@ -90,6 +91,7 @@ export class ResolverEndpoint extends cdk.Construct { securityGroupIds: [securityGroup.ref], name: `${this.props.name} Outbound Endpoint`, }); + this._outboundEndpoint.addDependsOn(securityGroup); return this._outboundEndpoint; } diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts index f64276d18..5991ff3be 100644 --- a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts @@ -151,4 +151,4 @@ async function getVpcIds(resolverRuleId: string) { const vpcIds = associations.ResolverRuleAssociations?.map(a => a.VPCId); return vpcIds; -} \ No newline at end of file +} From 492e3dd367f54505473a855eb997187cf6ca01e7 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Thu, 17 Sep 2020 11:43:36 +0530 Subject: [PATCH 12/25] Adding port for rule --- .../deployments/central-endpoints/step-2.ts | 3 +- src/lib/common/src/aws/dynamodb.ts | 31 ------------------- .../runtime/src/index.ts | 1 + 3 files changed, 3 insertions(+), 32 deletions(-) diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts index 6aedcbb91..1d4d26781 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts @@ -244,8 +244,9 @@ function domainToName(domain: string): string { export function createRuleName(name: string): string { const hash = hashPath([name], 8); if (name.length > 44) { - name = name.substring(0, 44) + hash; + name = name.substring(0, 44); } + name = name + hash; return createName({ name, }); diff --git a/src/lib/common/src/aws/dynamodb.ts b/src/lib/common/src/aws/dynamodb.ts index 0919bdf0d..3f110d200 100644 --- a/src/lib/common/src/aws/dynamodb.ts +++ b/src/lib/common/src/aws/dynamodb.ts @@ -2,13 +2,6 @@ import aws from './aws-client'; import * as dynamodb from 'aws-sdk/clients/dynamodb'; import { throttlingBackOff } from './backoff'; -interface Attribute { - key: string; - value: string; - name: string; - type: 'S' | 'N' | 'B'; -} - export class DynamoDB { private readonly client: aws.DynamoDB; @@ -67,28 +60,4 @@ export class DynamoDB { async updateItem(props: dynamodb.UpdateItemInput): Promise { await throttlingBackOff(() => this.client.updateItem(props).promise()); } - - getUpdateValueInput(attributes: Attribute[]) { - if (attributes.length === 0) { - return; - } - const expAttributeNames: aws.DynamoDB.ExpressionAttributeNameMap = {}; - const expAttributeValues: aws.DynamoDB.ExpressionAttributeValueMap = {}; - let updateExpression: string = 'set '; - for (const att of attributes) { - const attributeValue: aws.DynamoDB.AttributeValue = {}; - expAttributeNames[`#${att.key}`] = att.name; - attributeValue[att.type] = att.value; - expAttributeValues[`:${att.key}`] = attributeValue; - updateExpression += `#${att.key} = :${att.key},`; - } - // Remove "," if exists as last character - updateExpression = updateExpression.endsWith(',') ? updateExpression.slice(0, -1) : updateExpression; - - return { - ExpressionAttributeNames: expAttributeNames, - UpdateExpression: updateExpression, - ExpressionAttributeValues: expAttributeValues, - }; - } } diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts index 5991ff3be..d87d54e12 100644 --- a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts @@ -38,6 +38,7 @@ async function onCreateOrUpdate(event: CloudFormationCustomResourceEvent) { targetIps.forEach(ip => { targetIpParams.push({ Ip: ip, + Port: 53, }); }); let resolverRuleId: string; From 6d5bcf65cb5c2170bd59ac11acaa1118aac0fa83 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Thu, 17 Sep 2020 11:58:35 +0530 Subject: [PATCH 13/25] Adding PORT to rule --- .../src/deployments/central-endpoints/step-2.ts | 14 +++++++++++--- .../cdk-create-resolver-rule/cdk/index.ts | 7 ++++++- .../cdk-create-resolver-rule/runtime/src/index.ts | 11 ++--------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts index 1d4d26781..4d74491de 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts @@ -13,7 +13,7 @@ import { JsonOutputValue } from '../../common/json-output'; import { Account, getAccountId } from '../../utils/accounts'; import * as ram from '@aws-cdk/aws-ram'; import { createName, hashPath } from '@aws-accelerator/cdk-accelerator/src/core/accelerator-name-generator'; -import { CreateResolverRule } from '@aws-accelerator/custom-resource-create-resolver-rule'; +import { CreateResolverRule, TargetIp } from '@aws-accelerator/custom-resource-create-resolver-rule'; import { IamRoleOutputFinder } from '@aws-accelerator/common-outputs/src/iam-role'; export interface CentralEndpointsStep2Props { @@ -140,11 +140,15 @@ export async function step2(props: CentralEndpointsStep2Props) { // For each on-premise domain defined in the parameters file, create a Resolver rule which points to the specified IP's for (const onPremRuleConfig of vpcConfig['on-premise-rules'] || []) { + const targetIps: TargetIp[] = onPremRuleConfig['outbound-ips'].map((ip) => ({ + Ip: ip, + Port: 53 + })); const rule = new CreateResolverRule(accountStack, `${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}`, { domainName: onPremRuleConfig.zone, resolverEndpointId: r53ResolverEndpoints.outboundEndpointRef!, roleArn: roleOutput.roleArn, - targetIps: onPremRuleConfig['outbound-ips'], + targetIps: targetIps, vpcId: vpcOutput.vpcId, name: createRuleName(`${vpcConfig.name}-onprem-${domainToName(onPremRuleConfig.zone)}`), }); @@ -171,12 +175,16 @@ export async function step2(props: CentralEndpointsStep2Props) { continue; } madIPs = madOutput[0].dnsIps.split(','); + const targetIps: TargetIp[] = madIPs.map((ip) => ({ + Ip: ip, + Port: 53 + })); const madRule = new CreateResolverRule(accountStack, `${domainToName(mad['dns-domain'])}-${vpcConfig.name}`, { domainName: mad['dns-domain'], resolverEndpointId: r53ResolverEndpoints.outboundEndpointRef!, roleArn: roleOutput.roleArn, - targetIps: madIPs, + targetIps: targetIps, vpcId: vpcOutput.vpcId, name: createRuleName(`${vpcConfig.name}-mad-${domainToName(mad['dns-domain'])}`), }); diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/cdk/index.ts b/src/lib/custom-resources/cdk-create-resolver-rule/cdk/index.ts index 86306ed98..19bd17598 100644 --- a/src/lib/custom-resources/cdk-create-resolver-rule/cdk/index.ts +++ b/src/lib/custom-resources/cdk-create-resolver-rule/cdk/index.ts @@ -5,10 +5,15 @@ import * as lambda from '@aws-cdk/aws-lambda'; const resourceType = 'Custom::CreateResolverRule'; +export interface TargetIp { + Ip: string; + Port: number; +} + export interface CreateResolverRuleProps { vpcId: string; domainName: string; - targetIps: string[]; + targetIps: TargetIp[]; resolverEndpointId: string; name: string; roleArn: string; diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts index d87d54e12..082c5e3c6 100644 --- a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts @@ -7,7 +7,7 @@ import { delay, throttlingBackOff } from '@aws-accelerator/custom-resource-cfn-u export interface HandlerProperties { vpcId: string; domainName: string; - targetIps: string[]; + targetIps: AWS.Route53Resolver.TargetAddress[]; resolverEndpointId: string; name: string; } @@ -34,13 +34,6 @@ async function onEvent(event: CloudFormationCustomResourceEvent) { async function onCreateOrUpdate(event: CloudFormationCustomResourceEvent) { const properties = (event.ResourceProperties as unknown) as HandlerProperties; const { targetIps, vpcId, domainName, resolverEndpointId, name } = properties; - const targetIpParams: AWS.Route53Resolver.TargetAddress[] = []; - targetIps.forEach(ip => { - targetIpParams.push({ - Ip: ip, - Port: 53, - }); - }); let resolverRuleId: string; try { const ruleResponse = await throttlingBackOff(() => @@ -50,7 +43,7 @@ async function onCreateOrUpdate(event: CloudFormationCustomResourceEvent) { CreatorRequestId: name, RuleType: 'FORWARD', ResolverEndpointId: resolverEndpointId, - TargetIps: targetIpParams, + TargetIps: targetIps, Name: name, }) .promise(), From 75c768be66fc6072459c974fd44169df85c3ec84 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Thu, 17 Sep 2020 11:58:55 +0530 Subject: [PATCH 14/25] prettier --- .../cdk/src/deployments/central-endpoints/step-2.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts index 4d74491de..12bf4f7a4 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts @@ -140,9 +140,9 @@ export async function step2(props: CentralEndpointsStep2Props) { // For each on-premise domain defined in the parameters file, create a Resolver rule which points to the specified IP's for (const onPremRuleConfig of vpcConfig['on-premise-rules'] || []) { - const targetIps: TargetIp[] = onPremRuleConfig['outbound-ips'].map((ip) => ({ + const targetIps: TargetIp[] = onPremRuleConfig['outbound-ips'].map(ip => ({ Ip: ip, - Port: 53 + Port: 53, })); const rule = new CreateResolverRule(accountStack, `${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}`, { domainName: onPremRuleConfig.zone, @@ -175,9 +175,9 @@ export async function step2(props: CentralEndpointsStep2Props) { continue; } madIPs = madOutput[0].dnsIps.split(','); - const targetIps: TargetIp[] = madIPs.map((ip) => ({ + const targetIps: TargetIp[] = madIPs.map(ip => ({ Ip: ip, - Port: 53 + Port: 53, })); const madRule = new CreateResolverRule(accountStack, `${domainToName(mad['dns-domain'])}-${vpcConfig.name}`, { From 75240a4812b10e2ea38c532bb08132858755d85b Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Thu, 17 Sep 2020 13:55:48 +0530 Subject: [PATCH 15/25] Adding update for ResolverRule --- .../runtime/src/index.ts | 54 +++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts index 082c5e3c6..8deb9629a 100644 --- a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts @@ -1,6 +1,6 @@ import * as AWS from 'aws-sdk'; AWS.config.logger = console; -import { CloudFormationCustomResourceEvent, CloudFormationCustomResourceDeleteEvent } from 'aws-lambda'; +import { CloudFormationCustomResourceEvent, CloudFormationCustomResourceDeleteEvent, CloudFormationCustomResourceCreateEvent, CloudFormationCustomResourceUpdateEvent } from 'aws-lambda'; import { errorHandler } from '@aws-accelerator/custom-resource-runtime-cfn-response'; import { delay, throttlingBackOff } from '@aws-accelerator/custom-resource-cfn-utils'; @@ -23,15 +23,15 @@ async function onEvent(event: CloudFormationCustomResourceEvent) { // tslint:disable-next-line: switch-default switch (event.RequestType) { case 'Create': - return onCreateOrUpdate(event); + return onCreate(event); case 'Update': - return onCreateOrUpdate(event); + return onUpdate(event); case 'Delete': return onDelete(event); } } -async function onCreateOrUpdate(event: CloudFormationCustomResourceEvent) { +async function onCreate(event: CloudFormationCustomResourceCreateEvent) { const properties = (event.ResourceProperties as unknown) as HandlerProperties; const { targetIps, vpcId, domainName, resolverEndpointId, name } = properties; let resolverRuleId: string; @@ -76,6 +76,52 @@ async function onCreateOrUpdate(event: CloudFormationCustomResourceEvent) { }; } + +async function onUpdate(event: CloudFormationCustomResourceUpdateEvent) { + const properties = (event.ResourceProperties as unknown) as HandlerProperties; + const { targetIps, domainName, resolverEndpointId, name } = properties; + let resolverRuleId: string; + try { + const ruleResponse = await throttlingBackOff(() => route53Resolver.listResolverRules({ + Filters: [ + { + Name: 'ResolverEndpointId', + Values: [resolverEndpointId], + }, + { + Name: 'DomainName', + Values: [domainName], + }, + { + Name: 'Name', + Values: [name], + } + ] + }).promise()); + const updateRule = await throttlingBackOff(() => + route53Resolver + .updateResolverRule({ + Config: { + TargetIps: targetIps, + }, + ResolverRuleId: ruleResponse.ResolverRules?.[0].Id!, + }) + .promise(), + ); + resolverRuleId = updateRule.ResolverRule?.Id!; + } catch (error) { + // TODO: Handle Errors + throw new Error(error); + } + + return { + physicalResourceId: `CreateResolverRule-${resolverRuleId!}`, + data: { + RuleId: resolverRuleId, + }, + }; +} + async function onDelete(event: CloudFormationCustomResourceDeleteEvent) { console.log(`Deleting Resolver Rule...`); console.log(JSON.stringify(event, null, 2)); From d767f1d18214d6fa55cdb2c05384041c5f0f1240 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Thu, 17 Sep 2020 13:56:01 +0530 Subject: [PATCH 16/25] Prettier --- .../runtime/src/index.ts | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts index 8deb9629a..4d95baf94 100644 --- a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts @@ -1,6 +1,11 @@ import * as AWS from 'aws-sdk'; AWS.config.logger = console; -import { CloudFormationCustomResourceEvent, CloudFormationCustomResourceDeleteEvent, CloudFormationCustomResourceCreateEvent, CloudFormationCustomResourceUpdateEvent } from 'aws-lambda'; +import { + CloudFormationCustomResourceEvent, + CloudFormationCustomResourceDeleteEvent, + CloudFormationCustomResourceCreateEvent, + CloudFormationCustomResourceUpdateEvent, +} from 'aws-lambda'; import { errorHandler } from '@aws-accelerator/custom-resource-runtime-cfn-response'; import { delay, throttlingBackOff } from '@aws-accelerator/custom-resource-cfn-utils'; @@ -76,28 +81,31 @@ async function onCreate(event: CloudFormationCustomResourceCreateEvent) { }; } - async function onUpdate(event: CloudFormationCustomResourceUpdateEvent) { const properties = (event.ResourceProperties as unknown) as HandlerProperties; const { targetIps, domainName, resolverEndpointId, name } = properties; let resolverRuleId: string; try { - const ruleResponse = await throttlingBackOff(() => route53Resolver.listResolverRules({ - Filters: [ - { - Name: 'ResolverEndpointId', - Values: [resolverEndpointId], - }, - { - Name: 'DomainName', - Values: [domainName], - }, - { - Name: 'Name', - Values: [name], - } - ] - }).promise()); + const ruleResponse = await throttlingBackOff(() => + route53Resolver + .listResolverRules({ + Filters: [ + { + Name: 'ResolverEndpointId', + Values: [resolverEndpointId], + }, + { + Name: 'DomainName', + Values: [domainName], + }, + { + Name: 'Name', + Values: [name], + }, + ], + }) + .promise(), + ); const updateRule = await throttlingBackOff(() => route53Resolver .updateResolverRule({ From 6cfe471d2d636556b97fce11aac0313069303cb8 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Thu, 17 Sep 2020 16:34:04 +0530 Subject: [PATCH 17/25] Changes for max resources --- .../deployments/central-endpoints/step-2.ts | 243 ++++++++++++++---- .../deployments/central-endpoints/step-3.ts | 123 +++++++-- .../deployments/central-endpoints/step-4.ts | 15 +- .../runtime/src/index.ts | 2 +- 4 files changed, 307 insertions(+), 76 deletions(-) diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts index 12bf4f7a4..e2e37202d 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts @@ -8,13 +8,25 @@ import { StackOutput, } from '@aws-accelerator/common-outputs/src/stack-output'; import { VpcOutputFinder } from '@aws-accelerator/common-outputs/src/vpc'; -import { ResolverEndpoint, ResolverRule } from '@aws-accelerator/cdk-constructs/src/route53'; +import { ResolverEndpoint } from '@aws-accelerator/cdk-constructs/src/route53'; import { JsonOutputValue } from '../../common/json-output'; import { Account, getAccountId } from '../../utils/accounts'; import * as ram from '@aws-cdk/aws-ram'; import { createName, hashPath } from '@aws-accelerator/cdk-accelerator/src/core/accelerator-name-generator'; import { CreateResolverRule, TargetIp } from '@aws-accelerator/custom-resource-create-resolver-rule'; import { IamRoleOutputFinder } from '@aws-accelerator/common-outputs/src/iam-role'; +import { + StaticResourcesOutput, + StaticResourcesOutputFinder, +} from '@aws-accelerator/common-outputs/src/static-resource'; +import { CfnStaticResourcesOutput } from './outputs'; + +// Changing these values will lead to redeploying all Phase-3 Endpoint stacks +const MAX_RESOURCES_IN_STACK = 10; +const RESOURCE_TYPE = 'ResolverEndpointAndRule'; +const CENTRAL_VPC_RESOURCE_TYPE = 'CentralVpcResolverEndpointAndRule'; +const STACK_COMMON_SUFFIX = 'ResolverEndpoints'; +const STACK_CENTRAL_VPC_COMMON_SUFFIX = 'CentralVpcResolverEndpoints'; export interface CentralEndpointsStep2Props { accountStacks: AccountStacks; @@ -36,6 +48,37 @@ export async function step2(props: CentralEndpointsStep2Props) { const madConfigs = config.getMadConfigs(); const zonesConfig = config['global-options'].zones; const accountRulesCounter: { [accountKey: string]: number } = {}; + + const allStaticResources: StaticResourcesOutput[] = StaticResourcesOutputFinder.findAll({ + outputs, + }).filter(sr => sr.resourceType === RESOURCE_TYPE); + + const centralVpcStaticResources: StaticResourcesOutput[] = StaticResourcesOutputFinder.findAll({ + outputs, + }).filter(sr => sr.resourceType === CENTRAL_VPC_RESOURCE_TYPE); + + // Initiate previous stacks to handle deletion of previously deployed stack if there are no resources + for (const sr of allStaticResources) { + accountStacks.tryGetOrCreateAccountStack(sr.accountKey, sr.region, `${STACK_COMMON_SUFFIX}-${sr.suffix}`); + } + + // Initiate previous stacks to handle deletion of previously deployed stack if there are no resources + for (const sr of centralVpcStaticResources) { + accountStacks.tryGetOrCreateAccountStack(sr.accountKey, sr.region, STACK_CENTRAL_VPC_COMMON_SUFFIX); + } + + const accountStaticResourcesConfig: { [accountKey: string]: StaticResourcesOutput[] } = {}; + const accountRegionExistingResources: { + [accountKey: string]: { + [region: string]: string[]; + }; + } = {}; + const accountRegionMaxSuffix: { + [accountKey: string]: { + [region: string]: number; + }; + } = {}; + for (const { accountKey, vpcConfig } of vpcConfigs) { const resolversConfig = vpcConfig.resolvers; if (!resolversConfig) { @@ -86,23 +129,66 @@ export async function step2(props: CentralEndpointsStep2Props) { continue; } + let suffix: number; let stackSuffix: string; - if ( - !!zonesConfig.find( - zc => zc.account === accountKey && zc['resolver-vpc'] === vpcConfig.name && zc.region === vpcConfig.region, - ) - ) { - stackSuffix = `EndpointsRules-${vpcConfig.name}`; + let newResource = true; + const constructName = `${STACK_COMMON_SUFFIX}-${vpcConfig.name}`; + + // Load all account stacks to object + if (!accountStaticResourcesConfig[accountKey]) { + accountStaticResourcesConfig[accountKey] = allStaticResources.filter(sr => sr.accountKey === accountKey); + } + if (!accountRegionMaxSuffix[accountKey]) { + accountRegionMaxSuffix[accountKey] = {}; + } + + // Load Max suffix for each region of account to object + if (!accountRegionMaxSuffix[accountKey][vpcConfig.region]) { + const localSuffix = accountStaticResourcesConfig[accountKey] + .filter(sr => sr.region === vpcConfig.region) + .flatMap(r => r.suffix); + accountRegionMaxSuffix[accountKey][vpcConfig.region] = localSuffix.length === 0 ? 1 : Math.max(...localSuffix); + } + + if (!accountRegionExistingResources[accountKey]) { + const localRegionalResources = accountStaticResourcesConfig[accountKey] + .filter(sr => sr.region === vpcConfig.region) + .flatMap(sr => sr.resources); + accountRegionExistingResources[accountKey] = {}; + accountRegionExistingResources[accountKey][vpcConfig.region] = localRegionalResources; + } else if (!accountRegionExistingResources[accountKey][vpcConfig.region]) { + const localRegionalResources = accountStaticResourcesConfig[accountKey] + .filter(sr => sr.region === vpcConfig.region) + .flatMap(sr => sr.resources); + accountRegionExistingResources[accountKey][vpcConfig.region] = localRegionalResources; + } + + suffix = accountRegionMaxSuffix[accountKey][vpcConfig.region]; + stackSuffix = `${STACK_COMMON_SUFFIX}-${suffix}`; + const centralVpc = !!zonesConfig.find( + zc => zc.account === accountKey && zc['resolver-vpc'] === vpcConfig.name && zc.region === vpcConfig.region, + ); + if (centralVpc) { + stackSuffix = STACK_CENTRAL_VPC_COMMON_SUFFIX; } else { - if (accountRulesCounter[`${accountKey}-${vpcConfig.region}`]) { - accountRulesCounter[`${accountKey}-${vpcConfig.region}`] = ++accountRulesCounter[ - `${accountKey}-${vpcConfig.region}` - ]; + if (accountRegionExistingResources[accountKey][vpcConfig.region].includes(constructName)) { + newResource = false; + const regionStacks = accountStaticResourcesConfig[accountKey].filter(sr => sr.region === vpcConfig.region); + for (const rs of regionStacks) { + if (rs.resources.includes(constructName)) { + stackSuffix = `${STACK_COMMON_SUFFIX}-${rs.suffix}`; + break; + } + } } else { - accountRulesCounter[`${accountKey}-${vpcConfig.region}`] = 1; + const existingResources = accountStaticResourcesConfig[accountKey].find( + sr => sr.region === vpcConfig.region && sr.suffix === suffix, + ); + if (existingResources && existingResources.resources.length >= MAX_RESOURCES_IN_STACK) { + accountRegionMaxSuffix[accountKey][vpcConfig.region] = ++suffix; + } + stackSuffix = `${STACK_COMMON_SUFFIX}-${suffix}`; } - // Includes max of 10 VPCs, since we need max 8 resources for one VPC - stackSuffix = `EndpointsRules-${Math.ceil(accountRulesCounter[`${accountKey}-${vpcConfig.region}`] / 10)}`; } const accountStack = accountStacks.tryGetOrCreateAccountStack(accountKey, vpcConfig.region, stackSuffix); @@ -114,7 +200,7 @@ export async function step2(props: CentralEndpointsStep2Props) { // Call r53-resolver-endpoint per Account const r53ResolverEndpoints = new ResolverEndpoint( accountStack, - `ResolverEndpoints-${accountKey}-${vpcConfig.name}`, + `${STACK_COMMON_SUFFIX}-${accountKey}-${vpcConfig.name}`, { vpcId: vpcOutput.vpcId, name: vpcConfig.name, @@ -148,7 +234,7 @@ export async function step2(props: CentralEndpointsStep2Props) { domainName: onPremRuleConfig.zone, resolverEndpointId: r53ResolverEndpoints.outboundEndpointRef!, roleArn: roleOutput.roleArn, - targetIps: targetIps, + targetIps, vpcId: vpcOutput.vpcId, name: createRuleName(`${vpcConfig.name}-onprem-${domainToName(onPremRuleConfig.zone)}`), }); @@ -184,7 +270,7 @@ export async function step2(props: CentralEndpointsStep2Props) { domainName: mad['dns-domain'], resolverEndpointId: r53ResolverEndpoints.outboundEndpointRef!, roleArn: roleOutput.roleArn, - targetIps: targetIps, + targetIps, vpcId: vpcOutput.vpcId, name: createRuleName(`${vpcConfig.name}-mad-${domainToName(mad['dns-domain'])}`), }); @@ -200,48 +286,99 @@ export async function step2(props: CentralEndpointsStep2Props) { }); if (!isRuleShareNeeded) { - console.info(`VPC "${vpcConfig.name}" is not part of Central VPC under zones configuration`); - continue; + const regionVpcs = config + .getVpcConfigs() + .filter( + vc => + vc.vpcConfig.region === vpcConfig.region && + vc.vpcConfig['use-central-endpoints'] && + vc.accountKey !== accountKey, + ); + const sharedToAccountKeys = regionVpcs.map(rv => rv.accountKey); + const sharedToAccountIds: string[] = sharedToAccountKeys.map(accId => getAccountId(accounts, accId)!); + if (sharedToAccountIds.length > 0) { + const ruleArns: string[] = [ + ...madRules.map( + ruleId => `arn:aws:route53resolver:${vpcConfig.region}:${cdk.Aws.ACCOUNT_ID}:resolver-rule/${ruleId}`, + ), + ...onPremRules.map( + ruleId => `arn:aws:route53resolver:${vpcConfig.region}:${cdk.Aws.ACCOUNT_ID}:resolver-rule/${ruleId}`, + ), + ]; + + // share the route53 resolver rules + new ram.CfnResourceShare(accountStack, `ResolverRuleShare-${vpcConfig.name}`, { + name: createName({ + name: `${vpcConfig.name}-ResolverRules`, + }), + allowExternalPrincipals: false, + principals: sharedToAccountIds, + resourceArns: ruleArns, + }); + } } - const regionVpcs = config - .getVpcConfigs() - .filter( - vc => - vc.vpcConfig.region === vpcConfig.region && - vc.vpcConfig['use-central-endpoints'] && - vc.accountKey !== accountKey, + if (centralVpc) { + const currentResourcesObject = { + accountKey, + id: `${CENTRAL_VPC_RESOURCE_TYPE}-${vpcConfig.region}-${accountKey}-${suffix}`, + region: vpcConfig.region, + resourceType: CENTRAL_VPC_RESOURCE_TYPE, + resources: [`${STACK_CENTRAL_VPC_COMMON_SUFFIX}-${vpcConfig.name}`], + // Setting sufix to -1 since will only have one Central VPC per region + suffix: -1, + }; + new CfnStaticResourcesOutput( + accountStack, + `CentralVpcResolverEndpointsOutput-${vpcConfig.name}`, + currentResourcesObject, ); - if (!regionVpcs || regionVpcs.length === 0) { - console.info(`No VPCs to be shared with central Account VPC in region "${vpcConfig.region}"`); - continue; } - const sharedToAccountKeys = regionVpcs.map(rv => rv.accountKey); - const sharedToAccountIds: string[] = sharedToAccountKeys.map(accId => getAccountId(accounts, accId)!); - if (sharedToAccountIds.length === 0) { - console.info(`No Accounts exists for sharing Resolver Rules in region : ${vpcConfig.region}`); - continue; + if (newResource && !centralVpc) { + const currentSuffixIndex = allStaticResources.findIndex( + sr => sr.region === vpcConfig.region && sr.suffix === suffix && sr.accountKey === accountKey, + ); + const currentAccountSuffixIndex = accountStaticResourcesConfig[accountKey].findIndex( + sr => sr.region === vpcConfig.region && sr.suffix === suffix, + ); + if (currentSuffixIndex === -1) { + const currentResourcesObject = { + accountKey, + id: `${RESOURCE_TYPE}-${vpcConfig.region}-${accountKey}-${suffix}`, + region: vpcConfig.region, + resourceType: RESOURCE_TYPE, + resources: [constructName], + suffix, + }; + allStaticResources.push(currentResourcesObject); + accountStaticResourcesConfig[accountKey].push(currentResourcesObject); + } else { + const currentResourcesObject = allStaticResources[currentSuffixIndex]; + const currentAccountResourcesObject = accountStaticResourcesConfig[accountKey][currentAccountSuffixIndex]; + if (!currentResourcesObject.resources.includes(constructName)) { + currentResourcesObject.resources.push(constructName); + } + if (!currentAccountResourcesObject.resources.includes(constructName)) { + currentAccountResourcesObject.resources.push(constructName); + } + allStaticResources[currentSuffixIndex] = currentResourcesObject; + accountStaticResourcesConfig[accountKey][currentAccountSuffixIndex] = currentAccountResourcesObject; + } } - - const ruleArns: string[] = [ - ...madRules.map( - ruleId => `arn:aws:route53resolver:${vpcConfig.region}:${cdk.Aws.ACCOUNT_ID}:resolver-rule/${ruleId}`, - ), - ...onPremRules.map( - ruleId => `arn:aws:route53resolver:${vpcConfig.region}:${cdk.Aws.ACCOUNT_ID}:resolver-rule/${ruleId}`, - ), - ]; - - // share the route53 resolver rules - new ram.CfnResourceShare(accountStack, `ResolverRuleShare-${vpcConfig.name}`, { - name: createName({ - name: `${vpcConfig.name}-ResolverRules`, - }), - allowExternalPrincipals: false, - principals: sharedToAccountIds, - resourceArns: ruleArns, - }); + } + for (const sr of allStaticResources) { + const accountStack = accountStacks.tryGetOrCreateAccountStack( + sr.accountKey, + sr.region, + `${STACK_COMMON_SUFFIX}-${sr.suffix}`, + ); + if (!accountStack) { + throw new Error( + `Not able to get or create stack for ${sr.accountKey}: ${sr.region}: ${STACK_COMMON_SUFFIX}-${sr.suffix}`, + ); + } + new CfnStaticResourcesOutput(accountStack, `StaticResourceOutput-${sr.suffix}`, sr); } } diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-3.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-3.ts index c0e2a2b65..667b7087e 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-3.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-3.ts @@ -2,17 +2,18 @@ import { AccountStacks } from '../../common/account-stacks'; import { getStackJsonOutput, ResolversOutput, StackOutput } from '@aws-accelerator/common-outputs/src/stack-output'; import { AssociateResolverRules } from '@aws-accelerator/custom-resource-associate-resolver-rules'; import * as c from '@aws-accelerator/common-config'; -import * as route53resolver from '@aws-cdk/aws-route53resolver'; import { IamRoleOutputFinder } from '@aws-accelerator/common-outputs/src/iam-role'; import { VpcOutputFinder } from '@aws-accelerator/common-outputs/src/vpc'; import { StaticResourcesOutput, StaticResourcesOutputFinder, } from '@aws-accelerator/common-outputs/src/static-resource'; +import { CfnStaticResourcesOutput } from './outputs'; -// Changing this will result to redeploy most of the stack -const MAX_RESOURCES_IN_STACK = 2; -const RESOURCE_TYPE = 'ResolverRuleAssociation'; +// Changing these values will lead to redeploying all Phase-4 RuleAssociation stacks +const MAX_RESOURCES_IN_STACK = 190; +const RESOURCE_TYPE = 'ResolverRulesAssociation'; +const STACK_COMMON_SUFFIX = 'RulesAsscociation'; export interface CentralEndpointsStep3Props { accountStacks: AccountStacks; @@ -26,13 +27,16 @@ export interface CentralEndpointsStep3Props { export async function step3(props: CentralEndpointsStep3Props) { const { accountStacks, config, outputs } = props; const allVpcConfigs = config.getVpcConfigs(); - const accountRulesCounter: { [accountKey: string]: number } = {}; - const supportedregions = config['global-options']['supported-regions']; const allStaticResources: StaticResourcesOutput[] = StaticResourcesOutputFinder.findAll({ outputs, }).filter(sr => sr.resourceType === RESOURCE_TYPE); + // Initiate previous stacks to handle deletion of previously deployed stack if there are no resources + for (const sr of allStaticResources) { + accountStacks.tryGetOrCreateAccountStack(sr.accountKey, sr.region, `RulesAssc-${sr.suffix}`); + } + const accountStaticResourcesConfig: { [accountKey: string]: StaticResourcesOutput[] } = {}; const accountRegionExistingResources: { [accountKey: string]: { @@ -103,16 +107,60 @@ export async function step3(props: CentralEndpointsStep3Props) { continue; } - if (accountRulesCounter[`${accountKey}-${vpcConfig.region}`]) { - accountRulesCounter[`${accountKey}-${vpcConfig.region}`] = ++accountRulesCounter[ - `${accountKey}-${vpcConfig.region}` - ]; - } else { - accountRulesCounter[`${accountKey}-${vpcConfig.region}`] = 1; + let suffix: number; + let stackSuffix: string; + let newResource = true; + + // Load all account stacks to object + if (!accountStaticResourcesConfig[accountKey]) { + accountStaticResourcesConfig[accountKey] = allStaticResources.filter(sr => sr.accountKey === accountKey); + } + if (!accountRegionMaxSuffix[accountKey]) { + accountRegionMaxSuffix[accountKey] = {}; } - // Includes max of 50 VPCs, since we need 3 resource per VPC. - const stackSuffix = `RulesAssc-${Math.ceil(accountRulesCounter[`${accountKey}-${vpcConfig.region}`] / 150)}`; + // Load Max suffix for each region of account to object + if (!accountRegionMaxSuffix[accountKey][vpcConfig.region]) { + const localSuffix = accountStaticResourcesConfig[accountKey] + .filter(sr => sr.region === vpcConfig.region) + .flatMap(r => r.suffix); + accountRegionMaxSuffix[accountKey][vpcConfig.region] = localSuffix.length === 0 ? 1 : Math.max(...localSuffix); + } + + if (!accountRegionExistingResources[accountKey]) { + const localRegionalResources = accountStaticResourcesConfig[accountKey] + .filter(sr => sr.region === vpcConfig.region) + .flatMap(sr => sr.resources); + accountRegionExistingResources[accountKey] = {}; + accountRegionExistingResources[accountKey][vpcConfig.region] = localRegionalResources; + } else if (!accountRegionExistingResources[accountKey][vpcConfig.region]) { + const localRegionalResources = accountStaticResourcesConfig[accountKey] + .filter(sr => sr.region === vpcConfig.region) + .flatMap(sr => sr.resources); + accountRegionExistingResources[accountKey][vpcConfig.region] = localRegionalResources; + } + + suffix = accountRegionMaxSuffix[accountKey][vpcConfig.region]; + stackSuffix = `${STACK_COMMON_SUFFIX}-${suffix}`; + const constructName = `${STACK_COMMON_SUFFIX}-${vpcConfig.name}`; + if (accountRegionExistingResources[accountKey][vpcConfig.region].includes(constructName)) { + newResource = false; + const regionStacks = accountStaticResourcesConfig[accountKey].filter(sr => sr.region === vpcConfig.region); + for (const rs of regionStacks) { + if (rs.resources.includes(constructName)) { + stackSuffix = `${STACK_COMMON_SUFFIX}-${rs.suffix}`; + break; + } + } + } else { + const existingResources = accountStaticResourcesConfig[accountKey].find( + sr => sr.region === vpcConfig.region && sr.suffix === suffix, + ); + if (existingResources && existingResources.resources.length >= MAX_RESOURCES_IN_STACK) { + accountRegionMaxSuffix[accountKey][vpcConfig.region] = ++suffix; + } + stackSuffix = `${STACK_COMMON_SUFFIX}-${suffix}`; + } const accountStack = accountStacks.tryGetOrCreateAccountStack(accountKey, vpcConfig.region, stackSuffix); if (!accountStack) { @@ -130,10 +178,55 @@ export async function step3(props: CentralEndpointsStep3Props) { } const ruleIds = [...resolverRegionoutputs.rules?.madRules!, ...resolverRegionoutputs.rules?.onPremRules!]; - new AssociateResolverRules(accountStack, `Rule-Association-${vpcConfig.name}`, { + new AssociateResolverRules(accountStack, constructName, { resolverRuleIds: ruleIds, roleArn: roleOutput.roleArn, vpcId: vpcOutput.vpcId, }); + + if (newResource) { + const currentSuffixIndex = allStaticResources.findIndex( + sr => sr.region === vpcConfig.region && sr.suffix === suffix && sr.accountKey === accountKey, + ); + const currentAccountSuffixIndex = accountStaticResourcesConfig[accountKey].findIndex( + sr => sr.region === vpcConfig.region && sr.suffix === suffix, + ); + if (currentSuffixIndex === -1) { + const currentResourcesObject = { + accountKey, + id: `${STACK_COMMON_SUFFIX}-${vpcConfig.region}-${accountKey}-${suffix}`, + region: vpcConfig.region, + resourceType: RESOURCE_TYPE, + resources: [constructName], + suffix, + }; + allStaticResources.push(currentResourcesObject); + accountStaticResourcesConfig[accountKey].push(currentResourcesObject); + } else { + const currentResourcesObject = allStaticResources[currentSuffixIndex]; + const currentAccountResourcesObject = accountStaticResourcesConfig[accountKey][currentAccountSuffixIndex]; + if (!currentResourcesObject.resources.includes(constructName)) { + currentResourcesObject.resources.push(constructName); + } + if (!currentAccountResourcesObject.resources.includes(constructName)) { + currentAccountResourcesObject.resources.push(constructName); + } + allStaticResources[currentSuffixIndex] = currentResourcesObject; + accountStaticResourcesConfig[accountKey][currentAccountSuffixIndex] = currentAccountResourcesObject; + } + } + } + for (const sr of allStaticResources) { + const accountStack = accountStacks.tryGetOrCreateAccountStack( + sr.accountKey, + sr.region, + `${STACK_COMMON_SUFFIX}-${sr.suffix}`, + ); + if (!accountStack) { + throw new Error( + `Not able to get or create stack for ${sr.accountKey}: ${sr.region}: ${STACK_COMMON_SUFFIX}-${sr.suffix}`, + ); + } + new CfnStaticResourcesOutput(accountStack, `StaticResourceOutput-${sr.suffix}`, sr); } } diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-4.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-4.ts index b1cd36272..9adc6153f 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-4.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-4.ts @@ -13,8 +13,9 @@ import { import { CfnStaticResourcesOutput } from './outputs'; // Changing this will result to redeploy most of the stack -const MAX_RESOURCES_IN_STACK = 2; +const MAX_RESOURCES_IN_STACK = 190; const RESOURCE_TYPE = 'HostedZoneAssociation'; +const STACK_COMMON_SUFFIX = 'HostedZonesAssc'; export interface CentralEndpointsStep4Props { accountStacks: AccountStacks; @@ -58,7 +59,7 @@ export async function step4(props: CentralEndpointsStep4Props) { // Initiate previous stacks to handle deletion of previously deployed stack if there are no resources for (const sr of staticResources) { - accountStacks.tryGetOrCreateAccountStack(sr.accountKey, sr.region, `HostedZonesAssc-${sr.suffix}`); + accountStacks.tryGetOrCreateAccountStack(sr.accountKey, sr.region, `${STACK_COMMON_SUFFIX}-${sr.suffix}`); } const existingRegionResources: { [region: string]: string[] } = {}; @@ -108,7 +109,7 @@ export async function step4(props: CentralEndpointsStep4Props) { regionalMaxSuffix[vpcConfig.region] = ++suffix; } - let stackSuffix = `HostedZonesAssc-${suffix}`; + let stackSuffix = `${STACK_COMMON_SUFFIX}-${suffix}`; let updateOutputsRequired = true; const constructName = `AssociateHostedZones-${accountKey}-${vpcConfig.name}-${vpcConfig.region}`; const phzConstructName = `AssociatePrivateZones-${accountKey}-${vpcConfig.name}-${vpcConfig.region}`; @@ -117,7 +118,7 @@ export async function step4(props: CentralEndpointsStep4Props) { const regionStacks = staticResources.filter(sr => sr.region === vpcConfig.region); for (const rs of regionStacks) { if (rs.resources.includes(constructName)) { - stackSuffix = `HostedZonesAssc-${rs.suffix}`; + stackSuffix = `${STACK_COMMON_SUFFIX}-${rs.suffix}`; break; } } @@ -181,7 +182,7 @@ export async function step4(props: CentralEndpointsStep4Props) { if (currentSuffixIndex === -1) { const currentResourcesObject = { accountKey: masterAccountKey, - id: `AssociateHostedZones-${vpcConfig.region}-${masterAccountKey}-${suffix}`, + id: `${STACK_COMMON_SUFFIX}-${vpcConfig.region}-${masterAccountKey}-${suffix}`, region: vpcConfig.region, resourceType: RESOURCE_TYPE, resources: [constructName], @@ -203,11 +204,11 @@ export async function step4(props: CentralEndpointsStep4Props) { const accountStack = accountStacks.tryGetOrCreateAccountStack( sr.accountKey, sr.region, - `HostedZonesAssc-${sr.suffix}`, + `${STACK_COMMON_SUFFIX}-${sr.suffix}`, ); if (!accountStack) { throw new Error( - `Not able to get or create stack for ${sr.accountKey}: ${sr.region}: HostedZonesAssc-${sr.suffix}`, + `Not able to get or create stack for ${sr.accountKey}: ${sr.region}: ${STACK_COMMON_SUFFIX}-${sr.suffix}`, ); } new CfnStaticResourcesOutput(accountStack, `StaticResourceOutput-${sr.suffix}`, sr); diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts index 4d95baf94..cb38b5c49 100644 --- a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts @@ -135,7 +135,7 @@ async function onDelete(event: CloudFormationCustomResourceDeleteEvent) { console.log(JSON.stringify(event, null, 2)); const properties = (event.ResourceProperties as unknown) as HandlerProperties; const { resolverEndpointId, name } = properties; - let maxRetries = 20; + let maxRetries = 25; const resolverRule = await throttlingBackOff(() => route53Resolver .listResolverRules({ From 46b131cfc98bb41a62c958efcdd65f781fe7f764 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Thu, 17 Sep 2020 16:52:25 +0530 Subject: [PATCH 18/25] Fixing Tests --- src/deployments/cdk/src/apps/phase-4.ts | 2 -- .../unsupported-changed.spec.ts.snap | 16 ---------------- 2 files changed, 18 deletions(-) diff --git a/src/deployments/cdk/src/apps/phase-4.ts b/src/deployments/cdk/src/apps/phase-4.ts index 17b99550d..dc9f99cd9 100644 --- a/src/deployments/cdk/src/apps/phase-4.ts +++ b/src/deployments/cdk/src/apps/phase-4.ts @@ -2,8 +2,6 @@ import { PhaseInput } from './shared'; import * as securityHub from '../deployments/security-hub'; import * as cloudWatchDeployment from '../deployments/cloud-watch'; import * as centralEndpoints from '../deployments/central-endpoints'; -import { Context } from '@aws-cdk/aws-stepfunctions'; -import { loadStaticResources } from '../utils/static-resources'; export interface RdgwArtifactsOutput { accountKey: string; diff --git a/src/deployments/cdk/test/apps/__snapshots__/unsupported-changed.spec.ts.snap b/src/deployments/cdk/test/apps/__snapshots__/unsupported-changed.spec.ts.snap index 70f5fb983..52d3a6e60 100644 --- a/src/deployments/cdk/test/apps/__snapshots__/unsupported-changed.spec.ts.snap +++ b/src/deployments/cdk/test/apps/__snapshots__/unsupported-changed.spec.ts.snap @@ -630,8 +630,6 @@ exports[`there should not be any unsupported resource changes for AWS::Budgets:: exports[`there should not be any unsupported resource changes for AWS::Budgets::Budget: SharedNetworkPhase3 1`] = `Array []`; -exports[`there should not be any unsupported resource changes for AWS::Budgets::Budget: SharedNetworkPhase3EndpointsRules1 1`] = `Array []`; - exports[`there should not be any unsupported resource changes for AWS::Budgets::Budget: SharedServicesPhase0 1`] = `Array []`; exports[`there should not be any unsupported resource changes for AWS::Budgets::Budget: SharedServicesPhase1 1`] = `Array []`; @@ -777,8 +775,6 @@ exports[`there should not be any unsupported resource changes for AWS::Directory exports[`there should not be any unsupported resource changes for AWS::DirectoryService::MicrosoftAD: SharedNetworkPhase3 1`] = `Array []`; -exports[`there should not be any unsupported resource changes for AWS::DirectoryService::MicrosoftAD: SharedNetworkPhase3EndpointsRules1 1`] = `Array []`; - exports[`there should not be any unsupported resource changes for AWS::DirectoryService::MicrosoftAD: SharedServicesPhase0 1`] = `Array []`; exports[`there should not be any unsupported resource changes for AWS::DirectoryService::MicrosoftAD: SharedServicesPhase1 1`] = `Array []`; @@ -1044,8 +1040,6 @@ exports[`there should not be any unsupported resource changes for AWS::EC2::Inst exports[`there should not be any unsupported resource changes for AWS::EC2::Instance: SharedNetworkPhase3 1`] = `Array []`; -exports[`there should not be any unsupported resource changes for AWS::EC2::Instance: SharedNetworkPhase3EndpointsRules1 1`] = `Array []`; - exports[`there should not be any unsupported resource changes for AWS::EC2::Instance: SharedServicesPhase0 1`] = `Array []`; exports[`there should not be any unsupported resource changes for AWS::EC2::Instance: SharedServicesPhase1 1`] = `Array []`; @@ -1181,8 +1175,6 @@ exports[`there should not be any unsupported resource changes for AWS::EC2::Tran exports[`there should not be any unsupported resource changes for AWS::EC2::TransitGateway: SharedNetworkPhase3 1`] = `Array []`; -exports[`there should not be any unsupported resource changes for AWS::EC2::TransitGateway: SharedNetworkPhase3EndpointsRules1 1`] = `Array []`; - exports[`there should not be any unsupported resource changes for AWS::EC2::TransitGateway: SharedServicesPhase0 1`] = `Array []`; exports[`there should not be any unsupported resource changes for AWS::EC2::TransitGateway: SharedServicesPhase1 1`] = `Array []`; @@ -1314,8 +1306,6 @@ exports[`there should not be any unsupported resource changes for AWS::ElasticLo exports[`there should not be any unsupported resource changes for AWS::ElasticLoadBalancingV2::LoadBalancer: SharedNetworkPhase3 1`] = `Array []`; -exports[`there should not be any unsupported resource changes for AWS::ElasticLoadBalancingV2::LoadBalancer: SharedNetworkPhase3EndpointsRules1 1`] = `Array []`; - exports[`there should not be any unsupported resource changes for AWS::ElasticLoadBalancingV2::LoadBalancer: SharedServicesPhase0 1`] = `Array []`; exports[`there should not be any unsupported resource changes for AWS::ElasticLoadBalancingV2::LoadBalancer: SharedServicesPhase1 1`] = `Array []`; @@ -1523,8 +1513,6 @@ exports[`there should not be any unsupported resource changes for AWS::S3::Bucke exports[`there should not be any unsupported resource changes for AWS::S3::Bucket: SharedNetworkPhase3 1`] = `Array []`; -exports[`there should not be any unsupported resource changes for AWS::S3::Bucket: SharedNetworkPhase3EndpointsRules1 1`] = `Array []`; - exports[`there should not be any unsupported resource changes for AWS::S3::Bucket: SharedServicesPhase0 1`] = `Array []`; exports[`there should not be any unsupported resource changes for AWS::S3::Bucket: SharedServicesPhase1 1`] = `Array []`; @@ -1702,8 +1690,6 @@ exports[`there should not be any unsupported resource changes for AWS::SecretsMa exports[`there should not be any unsupported resource changes for AWS::SecretsManager::ResourcePolicy: SharedNetworkPhase3 1`] = `Array []`; -exports[`there should not be any unsupported resource changes for AWS::SecretsManager::ResourcePolicy: SharedNetworkPhase3EndpointsRules1 1`] = `Array []`; - exports[`there should not be any unsupported resource changes for AWS::SecretsManager::ResourcePolicy: SharedServicesPhase0 1`] = `Array []`; exports[`there should not be any unsupported resource changes for AWS::SecretsManager::ResourcePolicy: SharedServicesPhase1 1`] = `Array []`; @@ -1883,8 +1869,6 @@ exports[`there should not be any unsupported resource changes for AWS::SecretsMa exports[`there should not be any unsupported resource changes for AWS::SecretsManager::Secret: SharedNetworkPhase3 1`] = `Array []`; -exports[`there should not be any unsupported resource changes for AWS::SecretsManager::Secret: SharedNetworkPhase3EndpointsRules1 1`] = `Array []`; - exports[`there should not be any unsupported resource changes for AWS::SecretsManager::Secret: SharedServicesPhase0 1`] = `Array []`; exports[`there should not be any unsupported resource changes for AWS::SecretsManager::Secret: SharedServicesPhase1 1`] = `Array []`; From cd3b368119913a9090415daccc14cbfb8738360c Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Thu, 17 Sep 2020 17:32:26 +0530 Subject: [PATCH 19/25] Strict Error checking --- .../runtime/src/index.ts | 15 ++++-------- .../runtime/src/index.ts | 24 +++++++++++++++---- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts b/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts index 10828f2e2..324b05d6e 100644 --- a/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts @@ -132,7 +132,7 @@ async function onCreate(event: CloudFormationCustomResourceCreateEvent) { } else { // TODO Handle errors console.error(`Ignoring error while associating the hosted zone ${hostedZoneId} to VPC "${vpcName}"`); - console.error(e); + throw new Error(e); } } @@ -219,12 +219,12 @@ async function onUpdate(event: CloudFormationCustomResourceUpdateEvent) { console.log(`Disassociating hosted zone ${hostedZoneId} with VPC ${vpcId} ${vpcName}...`); await throttlingBackOff(() => vpcRoute53.disassociateVPCFromHostedZone(hostedZoneProps).promise()); } catch (e) { - if (e.code === 'ConflictingDomainExists') { - console.info('Domain already added; ignore this error and continue'); + if (e.code === 'VPCAssociationNotFound') { + console.warn(`The specified VPC "${vpcId}" and hosted zone "${hostedZoneId}" are not currently associated.`); } else { // TODO Handle errors console.error(`Ignoring error while associating the hosted zone ${hostedZoneId} to VPC "${vpcName}"`); - console.error(e); + throw new Error(e); } } @@ -277,13 +277,8 @@ async function onDelete(event: CloudFormationCustomResourceDeleteEvent) { console.log(`Disassociating hosted zone ${hostedZoneId} with VPC ${vpcId} ${vpcName}...`); await throttlingBackOff(() => vpcRoute53.disassociateVPCFromHostedZone(hostedZoneProps).promise()); } catch (e) { - if (e.code === 'ConflictingDomainExists') { - console.info('Domain already added; ignore this error and continue'); - } else { - // TODO Handle errors - console.error(`Ignoring error while associating the hosted zone ${hostedZoneId} to VPC "${vpcName}"`); + console.error(`Ignoring error while deleting Association and stack ${hostedZoneId} to VPC "${vpcName}"`); console.error(e); - } } // delete association of VPC with Hosted zones when VPC and Hosted Zones are defined in two different accounts diff --git a/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/src/index.ts b/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/src/index.ts index 03040b990..ff13de5dc 100644 --- a/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/src/index.ts @@ -47,7 +47,11 @@ async function onCreate(event: CloudFormationCustomResourceCreateEvent) { .promise(), ); } catch (error) { - console.error(error); + if (error.code === 'ResourceExistsException') { + console.warn(`Resolver Rule ${ruleId} is already Associated to ${vpcId}`); + } else { + throw new Error(error); + } } } return { @@ -73,7 +77,11 @@ async function onUpdate(event: CloudFormationCustomResourceUpdateEvent) { .promise(), ); } catch (error) { - console.error(error); + if (error.code === 'ResourceExistsException') { + console.warn(`Resolver Rule ${ruleId} is already Associated to ${vpcId}`); + } else { + throw new Error(error); + } } } @@ -88,7 +96,11 @@ async function onUpdate(event: CloudFormationCustomResourceUpdateEvent) { .promise(), ); } catch (error) { - console.error(error); + if (error.code === 'ResourceNotFoundException') { + console.warn(`Resolver Rule ${ruleId} is not Associated to ${vpcId}`); + } else { + throw new Error(error); + } } } @@ -114,7 +126,11 @@ async function onDelete(event: CloudFormationCustomResourceDeleteEvent) { .promise(), ); } catch (error) { - console.error(error); + if (error.code === 'ResourceNotFoundException') { + console.warn(`Resolver Rule ${ruleId} is not Associated to ${vpcId}`); + } else { + console.error(error); + } } } } From 522ac439f97d8cf4e9360b9306278b8d718222cc Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Thu, 17 Sep 2020 17:32:37 +0530 Subject: [PATCH 20/25] Prettier --- .../cdk-associate-hosted-zones/runtime/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts b/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts index 324b05d6e..b32c149a5 100644 --- a/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts @@ -277,8 +277,8 @@ async function onDelete(event: CloudFormationCustomResourceDeleteEvent) { console.log(`Disassociating hosted zone ${hostedZoneId} with VPC ${vpcId} ${vpcName}...`); await throttlingBackOff(() => vpcRoute53.disassociateVPCFromHostedZone(hostedZoneProps).promise()); } catch (e) { - console.error(`Ignoring error while deleting Association and stack ${hostedZoneId} to VPC "${vpcName}"`); - console.error(e); + console.error(`Ignoring error while deleting Association and stack ${hostedZoneId} to VPC "${vpcName}"`); + console.error(e); } // delete association of VPC with Hosted zones when VPC and Hosted Zones are defined in two different accounts From b42a6b14b3e0ab3db21c20dd1d8e30ba5faffe75 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Thu, 17 Sep 2020 18:44:37 +0530 Subject: [PATCH 21/25] Fixing CustomResource Delete action --- .../iam/central-endpoints-deployment-roles.ts | 1 + .../runtime/src/index.ts | 6 +++++- .../runtime/src/index.ts | 4 +++- .../runtime/src/index.ts | 18 ++++++++++++------ 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/deployments/cdk/src/deployments/iam/central-endpoints-deployment-roles.ts b/src/deployments/cdk/src/deployments/iam/central-endpoints-deployment-roles.ts index b2bff03c4..56057358c 100644 --- a/src/deployments/cdk/src/deployments/iam/central-endpoints-deployment-roles.ts +++ b/src/deployments/cdk/src/deployments/iam/central-endpoints-deployment-roles.ts @@ -46,6 +46,7 @@ export async function centralEndpointDeploymentRole(stack: AccountStack) { 'route53resolver:ListResolverRuleAssociations', 'route53resolver:CreateResolverRule', 'route53resolver:DisassociateResolverRule', + 'route53resolver:UpdateResolverRule', ], resources: ['*'], }), diff --git a/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts b/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts index b32c149a5..d7470d226 100644 --- a/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts @@ -244,7 +244,11 @@ async function onDelete(event: CloudFormationCustomResourceDeleteEvent) { console.log(JSON.stringify(event, null, 2)); const properties = (event.ResourceProperties as unknown) as HandlerProperties; const { assumeRoleName, hostedZoneAccountId, hostedZoneIds, vpcAccountId, vpcId, vpcName, vpcRegion } = properties; - + if ( + event.PhysicalResourceId != `AssociateHostedZones-${vpcName}-${vpcRegion}-${vpcAccountId}-${hostedZoneAccountId}` + ) { + return; + } const vpcAccountCredentials = await sts.getCredentialsForAccountAndRole(vpcAccountId, assumeRoleName); const vpcRoute53 = new AWS.Route53({ credentials: vpcAccountCredentials, diff --git a/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/src/index.ts b/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/src/index.ts index ff13de5dc..ae1c94ef7 100644 --- a/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/src/index.ts @@ -114,7 +114,9 @@ async function onDelete(event: CloudFormationCustomResourceDeleteEvent) { console.log(JSON.stringify(event, null, 2)); const properties = (event.ResourceProperties as unknown) as HandlerProperties; const { resolverRuleIds, vpcId } = properties; - + if (event.PhysicalResourceId != `AssociateResolverRules-${vpcId}`) { + return; + } for (const ruleId of resolverRuleIds) { try { await throttlingBackOff(() => diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts index cb38b5c49..84b42ecc0 100644 --- a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts @@ -152,14 +152,20 @@ async function onDelete(event: CloudFormationCustomResourceDeleteEvent) { }) .promise(), ); - for (const rule of resolverRule.ResolverRules! || []) { - let associatedVpcs = await getVpcIds(rule.Id!); - + if (!resolverRule.ResolverRules) { + return; + } + const ruleId = resolverRule.ResolverRules[0].Id; + if (!ruleId) { + return; + } + if (event.PhysicalResourceId === `CreateResolverRule-${ruleId}`) { + let associatedVpcs = await getVpcIds(ruleId!); for (const vpcId of associatedVpcs! || []) { await throttlingBackOff(() => route53Resolver .disassociateResolverRule({ - ResolverRuleId: rule.Id!, + ResolverRuleId: ruleId, VPCId: vpcId!, }) .promise(), @@ -167,7 +173,7 @@ async function onDelete(event: CloudFormationCustomResourceDeleteEvent) { } do { - associatedVpcs = await getVpcIds(rule.Id!); + associatedVpcs = await getVpcIds(ruleId); // Waiting to disassociate VPC Ids from the resolver rule await delay(5000); } while ((associatedVpcs || []).length > 0 && maxRetries-- > 0); @@ -175,7 +181,7 @@ async function onDelete(event: CloudFormationCustomResourceDeleteEvent) { await throttlingBackOff(() => route53Resolver .deleteResolverRule({ - ResolverRuleId: rule.Id!, + ResolverRuleId: ruleId, }) .promise(), ); From 9dfa2113defcf057f0743520f9e56914ef535912 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Thu, 17 Sep 2020 19:11:56 +0530 Subject: [PATCH 22/25] Updating documentation --- .../cdk-associate-hosted-zones/README.md | 44 ++++++------------- .../cdk-associate-resolver-rules/README.md | 37 +++------------- .../cdk-create-resolver-rule/README.md | 42 +++++------------- .../runtime/src/index.ts | 44 ++++++++++--------- 4 files changed, 54 insertions(+), 113 deletions(-) diff --git a/src/lib/custom-resources/cdk-associate-hosted-zones/README.md b/src/lib/custom-resources/cdk-associate-hosted-zones/README.md index dc91df26a..e3305d8be 100644 --- a/src/lib/custom-resources/cdk-associate-hosted-zones/README.md +++ b/src/lib/custom-resources/cdk-associate-hosted-zones/README.md @@ -1,36 +1,18 @@ -# Security Hub Enable Standards +# Associate Hosted Zones to VPC -This is a custom resource to enable Security Hub Standards and disable specific controls Used `describeStandards`, `batchEnableStandards`, `describeStandardControls` and `updateStandardControls` API calls. +This is a custom resource to associate vpc to Hosted Zone Used `createVPCAssociationAuthorization`, `associateVPCWithHostedZone`, `deleteVPCAssociationAuthorization` and `deleteVPCAssociationAuthorization` API calls. ## Usage - import { SecurityHubEnable } from '@aws-accelerator/custom-resource-security-hub-enable'; + import { AssociateHostedZones } from '@aws-accelerator/custom-resource-associate-hosted-zones'; - const enableSecurityHubResource = new SecurityHubEnable(this, 'EnableSecurityHubStandards`, { - standards: standards.standards, - }); - -## Input Example - - [ - { - "name": "AWS Foundational Security Best Practices v1.0.0", - "controls-to-disable": [ - "IAM.1" - ] - }, - { - "name": "PCI DSS v3.2.1", - "controls-to-disable": [ - "PCI.IAM.3", - "PCIDSS8.3.1" - ] - }, - { - "name": "CIS AWS Foundations Benchmark v1.2.0", - "controls-to-disable": [ - "CIS.1.3", - "CIS1.11" - ] - } - ] + new AssociateHostedZones(accountStack, constructName, { + assumeRoleName: assumeRole, + vpcAccountId, + vpcName: vpcConfig.name, + vpcId: vpcOutput.vpcId, + vpcRegion: vpcConfig.region, + hostedZoneAccountId, + hostedZoneIds, + roleArn, + }); diff --git a/src/lib/custom-resources/cdk-associate-resolver-rules/README.md b/src/lib/custom-resources/cdk-associate-resolver-rules/README.md index dc91df26a..668f5a397 100644 --- a/src/lib/custom-resources/cdk-associate-resolver-rules/README.md +++ b/src/lib/custom-resources/cdk-associate-resolver-rules/README.md @@ -1,36 +1,13 @@ -# Security Hub Enable Standards +# Associate Resolver Rule to VPC -This is a custom resource to enable Security Hub Standards and disable specific controls Used `describeStandards`, `batchEnableStandards`, `describeStandardControls` and `updateStandardControls` API calls. +This is a custom resource to Associate VPC to Resoulver Rule Used `associateResolverRule` and `disassociateResolverRule` API calls. ## Usage - import { SecurityHubEnable } from '@aws-accelerator/custom-resource-security-hub-enable'; + import { AssociateResolverRules } from '@aws-accelerator/custom-resource-associate-resolver-rules'; - const enableSecurityHubResource = new SecurityHubEnable(this, 'EnableSecurityHubStandards`, { - standards: standards.standards, + new AssociateResolverRules(accountStack, constructName, { + resolverRuleIds: ruleIds, + roleArn: roleOutput.roleArn, + vpcId: vpcOutput.vpcId, }); - -## Input Example - - [ - { - "name": "AWS Foundational Security Best Practices v1.0.0", - "controls-to-disable": [ - "IAM.1" - ] - }, - { - "name": "PCI DSS v3.2.1", - "controls-to-disable": [ - "PCI.IAM.3", - "PCIDSS8.3.1" - ] - }, - { - "name": "CIS AWS Foundations Benchmark v1.2.0", - "controls-to-disable": [ - "CIS.1.3", - "CIS1.11" - ] - } - ] diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/README.md b/src/lib/custom-resources/cdk-create-resolver-rule/README.md index dc91df26a..e4b25377d 100644 --- a/src/lib/custom-resources/cdk-create-resolver-rule/README.md +++ b/src/lib/custom-resources/cdk-create-resolver-rule/README.md @@ -1,36 +1,16 @@ -# Security Hub Enable Standards +# Create Resolver Rule -This is a custom resource to enable Security Hub Standards and disable specific controls Used `describeStandards`, `batchEnableStandards`, `describeStandardControls` and `updateStandardControls` API calls. +This is a custom resource to Create Resolver rule and associate to VPC Used `createResolverRule`, `associateResolverRule`, `listResolverRules`, `updateResolverRule`, `disassociateResolverRule`, `deleteResolverRule` and `listResolverRuleAssociations` API calls. ## Usage - import { SecurityHubEnable } from '@aws-accelerator/custom-resource-security-hub-enable'; + import { CreateResolverRule, TargetIp } from '@aws-accelerator/custom-resource-create-resolver-rule'; - const enableSecurityHubResource = new SecurityHubEnable(this, 'EnableSecurityHubStandards`, { - standards: standards.standards, - }); - -## Input Example - - [ - { - "name": "AWS Foundational Security Best Practices v1.0.0", - "controls-to-disable": [ - "IAM.1" - ] - }, - { - "name": "PCI DSS v3.2.1", - "controls-to-disable": [ - "PCI.IAM.3", - "PCIDSS8.3.1" - ] - }, - { - "name": "CIS AWS Foundations Benchmark v1.2.0", - "controls-to-disable": [ - "CIS.1.3", - "CIS1.11" - ] - } - ] + const rule = new CreateResolverRule(accountStack, `${domainToName(onPremRuleConfig.zone)}-${vpcConfig.name}`, { + domainName: onPremRuleConfig.zone, + resolverEndpointId: r53ResolverEndpoints.outboundEndpointRef!, + roleArn: roleOutput.roleArn, + targetIps, + vpcId: vpcOutput.vpcId, + name: createRuleName(`${vpcConfig.name}-onprem-${domainToName(onPremRuleConfig.zone)}`), + }); \ No newline at end of file diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts index 84b42ecc0..cbac9d0cd 100644 --- a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts @@ -74,7 +74,7 @@ async function onCreate(event: CloudFormationCustomResourceCreateEvent) { } return { - physicalResourceId: `CreateResolverRule-${resolverRuleId!}`, + physicalResourceId: name, data: { RuleId: resolverRuleId, }, @@ -123,7 +123,7 @@ async function onUpdate(event: CloudFormationCustomResourceUpdateEvent) { } return { - physicalResourceId: `CreateResolverRule-${resolverRuleId!}`, + physicalResourceId: name, data: { RuleId: resolverRuleId, }, @@ -136,6 +136,9 @@ async function onDelete(event: CloudFormationCustomResourceDeleteEvent) { const properties = (event.ResourceProperties as unknown) as HandlerProperties; const { resolverEndpointId, name } = properties; let maxRetries = 25; + if (event.PhysicalResourceId != name) { + return; + } const resolverRule = await throttlingBackOff(() => route53Resolver .listResolverRules({ @@ -159,33 +162,32 @@ async function onDelete(event: CloudFormationCustomResourceDeleteEvent) { if (!ruleId) { return; } - if (event.PhysicalResourceId === `CreateResolverRule-${ruleId}`) { - let associatedVpcs = await getVpcIds(ruleId!); - for (const vpcId of associatedVpcs! || []) { - await throttlingBackOff(() => - route53Resolver - .disassociateResolverRule({ - ResolverRuleId: ruleId, - VPCId: vpcId!, - }) - .promise(), - ); - } - - do { - associatedVpcs = await getVpcIds(ruleId); - // Waiting to disassociate VPC Ids from the resolver rule - await delay(5000); - } while ((associatedVpcs || []).length > 0 && maxRetries-- > 0); + let associatedVpcs = await getVpcIds(ruleId!); + for (const vpcId of associatedVpcs! || []) { await throttlingBackOff(() => route53Resolver - .deleteResolverRule({ + .disassociateResolverRule({ ResolverRuleId: ruleId, + VPCId: vpcId!, }) .promise(), ); } + + do { + associatedVpcs = await getVpcIds(ruleId); + // Waiting to disassociate VPC Ids from the resolver rule + await delay(5000); + } while ((associatedVpcs || []).length > 0 && maxRetries-- > 0); + + await throttlingBackOff(() => + route53Resolver + .deleteResolverRule({ + ResolverRuleId: ruleId, + }) + .promise(), + ); } async function getVpcIds(resolverRuleId: string) { From 593f66d605a9d8f44fa9aadee4b65b81ff65b1de Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Thu, 17 Sep 2020 20:13:16 +0530 Subject: [PATCH 23/25] Nasty Fix for resourceShare --- src/deployments/cdk/src/deployments/central-endpoints/step-2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts index e2e37202d..10186157a 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts @@ -285,7 +285,7 @@ export async function step2(props: CentralEndpointsStep2Props) { value: resolverOutput, }); - if (!isRuleShareNeeded) { + if (isRuleShareNeeded) { const regionVpcs = config .getVpcConfigs() .filter( From e6341508d651426cfdae8069cf96e8fb33924ea6 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Fri, 18 Sep 2020 11:59:02 +0530 Subject: [PATCH 24/25] Removing TODOs --- src/lib/cdk-constructs/src/route53/resolver-rule.ts | 1 - src/lib/custom-resources/cdk-associate-hosted-zones/cdk/index.ts | 1 - .../custom-resources/cdk-associate-resolver-rules/cdk/index.ts | 1 - src/lib/custom-resources/cdk-create-resolver-rule/cdk/index.ts | 1 - 4 files changed, 4 deletions(-) diff --git a/src/lib/cdk-constructs/src/route53/resolver-rule.ts b/src/lib/cdk-constructs/src/route53/resolver-rule.ts index d5b97777c..4fe4598d0 100644 --- a/src/lib/cdk-constructs/src/route53/resolver-rule.ts +++ b/src/lib/cdk-constructs/src/route53/resolver-rule.ts @@ -23,7 +23,6 @@ export class ResolverRule extends cdk.Construct { port: '53', })); - // TODO: Use custom resource to create Rule and Association to handle delete action this.rule = new r53Resolver.CfnResolverRule(this, 'Rule', { domainName: props.domain, ruleType: props.ruleType, diff --git a/src/lib/custom-resources/cdk-associate-hosted-zones/cdk/index.ts b/src/lib/custom-resources/cdk-associate-hosted-zones/cdk/index.ts index b1330a88a..b7c146276 100644 --- a/src/lib/custom-resources/cdk-associate-hosted-zones/cdk/index.ts +++ b/src/lib/custom-resources/cdk-associate-hosted-zones/cdk/index.ts @@ -46,7 +46,6 @@ export class AssociateHostedZones extends cdk.Construct { return existing as lambda.Function; } - // TODO: Use existing Lambda function to avoid creating multiple Lambda function in same account and region in different stacks const lambdaPath = require.resolve('@aws-accelerator/custom-resource-associate-hosted-zones-runtime'); const lambdaDir = path.dirname(lambdaPath); diff --git a/src/lib/custom-resources/cdk-associate-resolver-rules/cdk/index.ts b/src/lib/custom-resources/cdk-associate-resolver-rules/cdk/index.ts index 884914604..2ae4ca2ce 100644 --- a/src/lib/custom-resources/cdk-associate-resolver-rules/cdk/index.ts +++ b/src/lib/custom-resources/cdk-associate-resolver-rules/cdk/index.ts @@ -41,7 +41,6 @@ export class AssociateResolverRules extends cdk.Construct { return existing as lambda.Function; } - // TODO: Use existing Lambda function to avoid creating multiple Lambda function in same account and region in different stacks const lambdaPath = require.resolve('@aws-accelerator/custom-resource-associate-resolver-rules-runtime'); const lambdaDir = path.dirname(lambdaPath); diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/cdk/index.ts b/src/lib/custom-resources/cdk-create-resolver-rule/cdk/index.ts index 19bd17598..a874fdd18 100644 --- a/src/lib/custom-resources/cdk-create-resolver-rule/cdk/index.ts +++ b/src/lib/custom-resources/cdk-create-resolver-rule/cdk/index.ts @@ -53,7 +53,6 @@ export class CreateResolverRule extends cdk.Construct { return existing as lambda.Function; } - // TODO: Use existing Lambda function to avoid creating multiple Lambda function in same account and region in different stacks const lambdaPath = require.resolve('@aws-accelerator/custom-resource-create-resolver-rule-runtime'); const lambdaDir = path.dirname(lambdaPath); From 30f0851779744fa9df7d3c764e26c352300e95c2 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Fri, 18 Sep 2020 12:13:27 +0530 Subject: [PATCH 25/25] Updating Error messages --- .../cdk-associate-hosted-zones/runtime/src/index.ts | 8 ++++---- .../cdk-associate-resolver-rules/runtime/src/index.ts | 6 ++++++ .../cdk-create-resolver-rule/runtime/src/index.ts | 7 ++++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts b/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts index d7470d226..d222e1d25 100644 --- a/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts @@ -130,8 +130,8 @@ async function onCreate(event: CloudFormationCustomResourceCreateEvent) { if (e.code === 'ConflictingDomainExists') { console.info('Domain already added; ignore this error and continue'); } else { - // TODO Handle errors - console.error(`Ignoring error while associating the hosted zone ${hostedZoneId} to VPC "${vpcName}"`); + console.error(`Error while associating the hosted zone "${hostedZoneId}" to VPC "${vpcName}"`); + console.error(e); throw new Error(e); } } @@ -222,8 +222,8 @@ async function onUpdate(event: CloudFormationCustomResourceUpdateEvent) { if (e.code === 'VPCAssociationNotFound') { console.warn(`The specified VPC "${vpcId}" and hosted zone "${hostedZoneId}" are not currently associated.`); } else { - // TODO Handle errors - console.error(`Ignoring error while associating the hosted zone ${hostedZoneId} to VPC "${vpcName}"`); + console.error(`Error while associating the hosted zone "${hostedZoneId}" to VPC "${vpcName}"`); + console.error(e); throw new Error(e); } } diff --git a/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/src/index.ts b/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/src/index.ts index ae1c94ef7..e384851a5 100644 --- a/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/src/index.ts @@ -50,6 +50,8 @@ async function onCreate(event: CloudFormationCustomResourceCreateEvent) { if (error.code === 'ResourceExistsException') { console.warn(`Resolver Rule ${ruleId} is already Associated to ${vpcId}`); } else { + console.error(`Error while Associating Resolver Rule "${ruleId}" to VPC ${vpcId}`); + console.error(error); throw new Error(error); } } @@ -80,6 +82,8 @@ async function onUpdate(event: CloudFormationCustomResourceUpdateEvent) { if (error.code === 'ResourceExistsException') { console.warn(`Resolver Rule ${ruleId} is already Associated to ${vpcId}`); } else { + console.error(`Error while Associating Resolver Rule "${ruleId}" to VPC ${vpcId}`); + console.error(error); throw new Error(error); } } @@ -99,6 +103,8 @@ async function onUpdate(event: CloudFormationCustomResourceUpdateEvent) { if (error.code === 'ResourceNotFoundException') { console.warn(`Resolver Rule ${ruleId} is not Associated to ${vpcId}`); } else { + console.error(`Error while Disassociate VPC "${vpcId}" from Resolver Rule "${ruleId}"`); + console.error(error); throw new Error(error); } } diff --git a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts index cbac9d0cd..3756920d7 100644 --- a/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts @@ -55,7 +55,8 @@ async function onCreate(event: CloudFormationCustomResourceCreateEvent) { ); resolverRuleId = ruleResponse.ResolverRule?.Id!; } catch (error) { - // TODO: Handle Errors + console.error(`Error while Creating Resolver Rule "${name}"`); + console.error(error); throw new Error(error); } @@ -69,7 +70,6 @@ async function onCreate(event: CloudFormationCustomResourceCreateEvent) { .promise(), ); } catch (error) { - // TODO: Handle Errors console.log(error); } @@ -118,7 +118,8 @@ async function onUpdate(event: CloudFormationCustomResourceUpdateEvent) { ); resolverRuleId = updateRule.ResolverRule?.Id!; } catch (error) { - // TODO: Handle Errors + console.error(`Error while Updating Resolver Rule "${name}"`); + console.error(error); throw new Error(error); }