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 eeff9bc59..dc9f99cd9 100644 --- a/src/deployments/cdk/src/apps/phase-4.ts +++ b/src/deployments/cdk/src/apps/phase-4.ts @@ -2,7 +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'; export interface RdgwArtifactsOutput { accountKey: string; 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..10186157a 100644 --- a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts @@ -8,11 +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 } 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, 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; @@ -34,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) { @@ -67,6 +112,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,16 +129,67 @@ export async function step2(props: CentralEndpointsStep2Props) { 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; + 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] = {}; } - // Includes max of 15 VPCs, since we need max 11 resources for one VPC - const stackSuffix = `EndpointsRules-${Math.ceil(accountRulesCounter[`${accountKey}-${vpcConfig.region}`] / 15)}`; + // 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 (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) { @@ -95,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, @@ -121,19 +226,19 @@ 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, - }, - ); - rule.node.addDependency(r53ResolverEndpoints); + 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, + vpcId: vpcOutput.vpcId, + name: createRuleName(`${vpcConfig.name}-onprem-${domainToName(onPremRuleConfig.zone)}`), + }); + rule.node.addDependency(r53ResolverEndpoints.outboundEndpoint!); onPremRules.push(rule.ruleId); } resolverRulesOutput.onPremRules = onPremRules; @@ -156,18 +261,20 @@ 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 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, + vpcId: vpcOutput.vpcId, + name: createRuleName(`${vpcConfig.name}-mad-${domainToName(mad['dns-domain'])}`), + }); + madRule.node.addDependency(r53ResolverEndpoints.outboundEndpoint!); madRules.push(madRule.ruleId); } resolverRulesOutput.madRules = madRules; @@ -178,52 +285,114 @@ export async function step2(props: CentralEndpointsStep2Props) { value: resolverOutput, }); - if (!isRuleShareNeeded) { - console.info(`VPC "${vpcConfig.name}" is not part of Central VPC under zones configuration`); - continue; + if (isRuleShareNeeded) { + 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); } } 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); + } + name = name + hash; + return createName({ + name, + }); +} 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..667b7087e 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,19 @@ 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 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; @@ -16,7 +27,28 @@ export interface CentralEndpointsStep3Props { export async function step3(props: CentralEndpointsStep3Props) { const { accountStacks, config, outputs } = props; const allVpcConfigs = config.getVpcConfigs(); - const accountRulesCounter: { [accountKey: string]: number } = {}; + + 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]: { + [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']) { @@ -75,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] = {}; + } + + // 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; } - // Includes max of 50 VPCs, since we need 3 resource per VPC. - const stackSuffix = `RulesAssc-${Math.ceil(accountRulesCounter[`${accountKey}-${vpcConfig.region}`] / 50)}`; + 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) { @@ -92,12 +168,65 @@ export async function step3(props: CentralEndpointsStep3Props) { continue; } + const roleOutput = IamRoleOutputFinder.tryFindOneByName({ + outputs, + accountKey, + roleKey: 'CentralEndpointDeployment', + }); + if (!roleOutput) { + 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, - }); + 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 09f35c4d6..9adc6153f 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,16 @@ 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 { + 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 = 190; +const RESOURCE_TYPE = 'HostedZoneAssociation'; +const STACK_COMMON_SUFFIX = 'HostedZonesAssc'; export interface CentralEndpointsStep4Props { accountStacks: AccountStacks; @@ -42,7 +52,28 @@ export async function step4(props: CentralEndpointsStep4Props) { } } - const regionAssociationCounter: { [region: string]: number } = {}; + const staticResources: StaticResourcesOutput[] = StaticResourcesOutputFinder.findAll({ + outputs, + 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, `${STACK_COMMON_SUFFIX}-${sr.suffix}`); + } + + const existingRegionResources: { [region: string]: string[] } = {}; + const supportedregions = config['global-options']['supported-regions']; + + const regionalMaxSuffix: { [region: string]: number } = {}; + supportedregions.forEach(reg => { + 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).flatMap(r => r.resources); + }); for (const { accountKey, vpcConfig } of allVpcConfigs) { let seperateGlobalHostedZonesAccount = true; @@ -71,14 +102,27 @@ 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); + + if (existingResources && existingResources.resources.length >= MAX_RESOURCES_IN_STACK) { + 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 = `${STACK_COMMON_SUFFIX}-${suffix}`; + 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)) { + updateOutputsRequired = false; + const regionStacks = staticResources.filter(sr => sr.region === vpcConfig.region); + for (const rs of regionStacks) { + if (rs.resources.includes(constructName)) { + stackSuffix = `${STACK_COMMON_SUFFIX}-${rs.suffix}`; + break; + } + } + } const accountStack = accountStacks.tryGetOrCreateAccountStack(masterAccountKey, vpcConfig.region, stackSuffix); if (!accountStack) { @@ -98,43 +142,76 @@ 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}`, + }); + } + + // Update stackResources Object if new resource came + if (updateOutputsRequired) { + const currentSuffixIndex = staticResources.findIndex( + sr => sr.region === vpcConfig.region && sr.suffix === suffix, + ); + const vpcAssociationResources = [constructName]; + if (seperateGlobalHostedZonesAccount) { + vpcAssociationResources.push(phzConstructName); + } + if (currentSuffixIndex === -1) { + const currentResourcesObject = { + accountKey: masterAccountKey, + id: `${STACK_COMMON_SUFFIX}-${vpcConfig.region}-${masterAccountKey}-${suffix}`, + region: vpcConfig.region, + resourceType: RESOURCE_TYPE, + resources: [constructName], + suffix, + }; + if (seperateGlobalHostedZonesAccount) { + currentResourcesObject.resources.push(phzConstructName); + } + staticResources.push(currentResourcesObject); + } else { + const currentResourcesObject = staticResources[currentSuffixIndex]; + currentResourcesObject.resources.push(constructName); + staticResources[currentSuffixIndex] = currentResourcesObject; + } + } + } - 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}`, - }, + for (const sr of staticResources) { + 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/iam/central-endpoints-deployment-roles.ts b/src/deployments/cdk/src/deployments/iam/central-endpoints-deployment-roles.ts new file mode 100644 index 000000000..56057358c --- /dev/null +++ b/src/deployments/cdk/src/deployments/iam/central-endpoints-deployment-roles.ts @@ -0,0 +1,62 @@ +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', + 'route53resolver:UpdateResolverRule', + ], + 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/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 []`; 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', 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/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/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/common/src/aws/dynamodb.ts b/src/lib/common/src/aws/dynamodb.ts index 8b4430e91..3f110d200 100644 --- a/src/lib/common/src/aws/dynamodb.ts +++ b/src/lib/common/src/aws/dynamodb.ts @@ -24,6 +24,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; 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-hosted-zones/runtime/src/index.ts b/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts index 10828f2e2..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,9 +130,9 @@ 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); } } @@ -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(`Error while associating the hosted zone "${hostedZoneId}" to VPC "${vpcName}"`); console.error(e); + throw new Error(e); } } @@ -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, @@ -277,13 +281,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(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 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..668f5a397 --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-resolver-rules/README.md @@ -0,0 +1,13 @@ +# Associate Resolver Rule to VPC + +This is a custom resource to Associate VPC to Resoulver Rule Used `associateResolverRule` and `disassociateResolverRule` API calls. + +## Usage + + import { AssociateResolverRules } from '@aws-accelerator/custom-resource-associate-resolver-rules'; + + new AssociateResolverRules(accountStack, constructName, { + resolverRuleIds: ruleIds, + roleArn: roleOutput.roleArn, + vpcId: vpcOutput.vpcId, + }); 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..2ae4ca2ce --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-resolver-rules/cdk/index.ts @@ -0,0 +1,55 @@ +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; + } + + 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..e384851a5 --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-resolver-rules/runtime/src/index.ts @@ -0,0 +1,144 @@ +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) { + 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); + } + } + } + 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) { + 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); + } + } + } + + for (const ruleId of removeAssociations) { + try { + await throttlingBackOff(() => + route53Resolver + .disassociateResolverRule({ + ResolverRuleId: ruleId, + VPCId: vpcId, + }) + .promise(), + ); + } catch (error) { + 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); + } + } + } + + 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; + if (event.PhysicalResourceId != `AssociateResolverRules-${vpcId}`) { + return; + } + for (const ruleId of resolverRuleIds) { + try { + await throttlingBackOff(() => + route53Resolver + .disassociateResolverRule({ + ResolverRuleId: ruleId, + VPCId: vpcId, + }) + .promise(), + ); + } catch (error) { + if (error.code === 'ResourceNotFoundException') { + console.warn(`Resolver Rule ${ruleId} is not Associated to ${vpcId}`); + } else { + 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..e4b25377d --- /dev/null +++ b/src/lib/custom-resources/cdk-create-resolver-rule/README.md @@ -0,0 +1,16 @@ +# Create Resolver Rule + +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 { CreateResolverRule, TargetIp } from '@aws-accelerator/custom-resource-create-resolver-rule'; + + 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/cdk/index.ts b/src/lib/custom-resources/cdk-create-resolver-rule/cdk/index.ts new file mode 100644 index 000000000..a874fdd18 --- /dev/null +++ b/src/lib/custom-resources/cdk-create-resolver-rule/cdk/index.ts @@ -0,0 +1,67 @@ +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 TargetIp { + Ip: string; + Port: number; +} + +export interface CreateResolverRuleProps { + vpcId: string; + domainName: string; + targetIps: TargetIp[]; + 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; + } + + 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..3756920d7 --- /dev/null +++ b/src/lib/custom-resources/cdk-create-resolver-rule/runtime/src/index.ts @@ -0,0 +1,211 @@ +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 { delay, throttlingBackOff } from '@aws-accelerator/custom-resource-cfn-utils'; + +export interface HandlerProperties { + vpcId: string; + domainName: string; + targetIps: AWS.Route53Resolver.TargetAddress[]; + 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 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 { targetIps, vpcId, domainName, resolverEndpointId, name } = properties; + let resolverRuleId: string; + try { + const ruleResponse = await throttlingBackOff(() => + route53Resolver + .createResolverRule({ + DomainName: domainName, + CreatorRequestId: name, + RuleType: 'FORWARD', + ResolverEndpointId: resolverEndpointId, + TargetIps: targetIps, + Name: name, + }) + .promise(), + ); + resolverRuleId = ruleResponse.ResolverRule?.Id!; + } catch (error) { + console.error(`Error while Creating Resolver Rule "${name}"`); + console.error(error); + throw new Error(error); + } + + try { + await throttlingBackOff(() => + route53Resolver + .associateResolverRule({ + ResolverRuleId: resolverRuleId, + VPCId: vpcId, + }) + .promise(), + ); + } catch (error) { + console.log(error); + } + + return { + physicalResourceId: name, + data: { + RuleId: resolverRuleId, + }, + }; +} + +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) { + console.error(`Error while Updating Resolver Rule "${name}"`); + console.error(error); + throw new Error(error); + } + + return { + physicalResourceId: name, + 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 = 25; + if (event.PhysicalResourceId != name) { + return; + } + const resolverRule = await throttlingBackOff(() => + route53Resolver + .listResolverRules({ + Filters: [ + { + Name: 'ResolverEndpointId', + Values: [resolverEndpointId], + }, + { + Name: 'Name', + Values: [name], + }, + ], + }) + .promise(), + ); + if (!resolverRule.ResolverRules) { + return; + } + const ruleId = resolverRule.ResolverRules[0].Id; + if (!ruleId) { + return; + } + + 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); + + await throttlingBackOff(() => + route53Resolver + .deleteResolverRule({ + ResolverRuleId: ruleId, + }) + .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