diff --git a/reference-artifacts/config.ALZ.json b/reference-artifacts/config.ALZ.json index 61ccf5eae..bf58d30bd 100644 --- a/reference-artifacts/config.ALZ.json +++ b/reference-artifacts/config.ALZ.json @@ -83,14 +83,15 @@ "report-versioning": "OVERWRITE_REPORT" } }, - "zones": { + "zones": [{ "account": "shared-network", "resolver-vpc": "Endpoint", + "region": "ca-central-1", "names": { "public": ["dept.cloud-nuage.canada.ca"], "private": ["dept.cloud-nuage.gc.ca"] } - }, + }], "vpc-flow-logs": { "filter": "ALL", "interval": 60, diff --git a/reference-artifacts/config.example.json b/reference-artifacts/config.example.json index e61e96695..c89c3ba26 100644 --- a/reference-artifacts/config.example.json +++ b/reference-artifacts/config.example.json @@ -83,14 +83,15 @@ "report-versioning": "OVERWRITE_REPORT" } }, - "zones": { + "zones": [{ "account": "shared-network", "resolver-vpc": "Endpoint", + "region": "ca-central-1", "names": { "public": ["dept.cloud-nuage.canada.ca"], "private": ["dept.cloud-nuage.gc.ca"] } - }, + }], "vpc-flow-logs": { "filter": "ALL", "interval": 60, diff --git a/src/core/cdk/src/initial-setup.ts b/src/core/cdk/src/initial-setup.ts index 1b38f902a..74d3170d2 100644 --- a/src/core/cdk/src/initial-setup.ts +++ b/src/core/cdk/src/initial-setup.ts @@ -158,7 +158,7 @@ export namespace InitialSetup { functionPayload: { 'inputConfig.$': '$', region: cdk.Aws.REGION, - 'baseline.$': '$.configuration.baseline', + 'baseline.$': '$.configuration.baselineOutput.baseline', }, resultPath: 'DISCARD', }); @@ -174,8 +174,9 @@ export namespace InitialSetup { 'configFilePath.$': '$.configuration.configFilePath', 'configCommitId.$': '$.configuration.configCommitId', 'acceleratorVersion.$': '$.configuration.acceleratorVersion', + outputTableName: outputsTable.tableName, }, - resultPath: '$.configuration.baseline', + resultPath: '$.configuration.baselineOutput', }); const loadLandingZoneConfigurationTask = new CodeTask(this, 'Load Landing Zone Configuration', { @@ -188,7 +189,9 @@ export namespace InitialSetup { configRepositoryName: props.configRepositoryName, 'configFilePath.$': '$.configuration.configFilePath', 'configCommitId.$': '$.configuration.configCommitId', - 'baseline.$': '$.configuration.baseline', + 'baseline.$': '$.configuration.baselineOutput.baseline', + 'storeAllOutputs.$': '$.configuration.baselineOutput.storeAllOutputs', + 'phases.$': '$.configuration.baselineOutput.phases', 'acceleratorVersion.$': '$.configuration.acceleratorVersion', 'configRootFilePath.$': '$.configuration.configRootFilePath', }, @@ -205,7 +208,9 @@ export namespace InitialSetup { configRepositoryName: props.configRepositoryName, 'configFilePath.$': '$.configuration.configFilePath', 'configCommitId.$': '$.configuration.configCommitId', - 'baseline.$': '$.configuration.baseline', + 'baseline.$': '$.configuration.baselineOutput.baseline', + 'storeAllOutputs.$': '$.configuration.baselineOutput.storeAllOutputs', + 'phases.$': '$.configuration.baselineOutput.phases', 'acceleratorVersion.$': '$.configuration.acceleratorVersion', 'configRootFilePath.$': '$.configuration.configRootFilePath', }, @@ -332,6 +337,8 @@ export namespace InitialSetup { 'configCommitId.$': '$.configuration.configCommitId', 'acceleratorVersion.$': '$.configuration.acceleratorVersion', 'baseline.$': '$.configuration.baseline', + 'phases.$': '$.configuration.phases', + 'storeAllOutputs.$': '$.configuration.storeAllOutputs', 'regions.$': '$.configuration.regions', 'accounts.$': '$.configuration.accounts', 'configRootFilePath.$': '$.configuration.configRootFilePath', @@ -590,6 +597,43 @@ export namespace InitialSetup { return storeOutputsTask; }; + const storeAllPhaseOutputs = new sfn.Map(this, `Store All Phase Outputs Map`, { + itemsPath: '$.phases', + resultPath: 'DISCARD', + maxConcurrency: 1, + parameters: { + 'accounts.$': '$.accounts', + 'regions.$': '$.regions', + acceleratorPrefix: props.acceleratorPrefix, + assumeRoleName: props.stateMachineExecutionRole, + outputsTable: outputsTable.tableName, + configRepositoryName: props.configRepositoryName, + 'phaseNumber.$': '$$.Map.Item.Value', + 'configFilePath.$': '$.configFilePath', + 'configCommitId.$': '$.configCommitId', + }, + }); + + const storeAllOutputsTask = new sfn.Task(this, `Store All Phase Outputs`, { + // tslint:disable-next-line: deprecation + task: new tasks.StartExecution(storeOutputsStateMachine, { + integrationPattern: sfn.ServiceIntegrationPattern.SYNC, + input: { + 'accounts.$': '$.accounts', + 'regions.$': '$.regions', + acceleratorPrefix: props.acceleratorPrefix, + assumeRoleName: props.stateMachineExecutionRole, + outputsTable: outputsTable.tableName, + configRepositoryName: props.configRepositoryName, + 'phaseNumber.$': '$.phaseNumber', + 'configFilePath.$': '$.configFilePath', + 'configCommitId.$': '$.configCommitId', + }, + }), + resultPath: 'DISCARD', + }); + storeAllPhaseOutputs.iterator(storeAllOutputsTask); + // TODO Create separate state machine for deployment const deployPhaseRolesTask = createDeploymentTask(-1, false); const storePreviousOutput = createStoreOutputTask(-1); @@ -675,23 +719,6 @@ export namespace InitialSetup { resultPath: 'DISCARD', }); - const associateHostedZonesTask = new CodeTask(this, 'Associate Hosted Zones', { - functionProps: { - code: lambdaCode, - handler: 'index.associateHostedZonesStep', - role: pipelineRole, - }, - functionPayload: { - parametersTableName: parametersTable.tableName, - assumeRoleName: props.stateMachineExecutionRole, - 'configRepositoryName.$': '$.configRepositoryName', - 'configFilePath.$': '$.configFilePath', - 'configCommitId.$': '$.configCommitId', - outputTableName: outputsTable.tableName, - }, - resultPath: 'DISCARD', - }); - const addTagsToSharedResourcesTask = new CodeTask(this, 'Add Tags to Shared Resources', { functionProps: { code: lambdaCode, @@ -775,7 +802,6 @@ export namespace InitialSetup { .next(storePhase3Output) .next(deployPhase4Task) .next(storePhase4Output) - .next(associateHostedZonesTask) .next(addTagsToSharedResourcesTask) .next(enableDirectorySharingTask) .next(deployPhase5Task) @@ -788,18 +814,25 @@ export namespace InitialSetup { .otherwise(commonStep1) .afterwards(); + const commonStep2 = deployPhaseRolesTask + .next(storePreviousOutput) + .next(deployPhase0Task) + .next(storePhase0Output) + .next(verifyFilesTask) + .next(enableConfigChoice); + + const storeAllOutputsChoice = new sfn.Choice(this, 'Store All Phase Outputs?') + .when(sfn.Condition.booleanEquals('$.storeAllOutputs', true), storeAllPhaseOutputs.next(commonStep2)) + .otherwise(commonStep2) + .afterwards(); + const commonDefinition = loadOrganizationsTask.startState .next(loadAccountsTask) .next(installRolesTask) .next(deleteVpcTask) .next(loadLimitsTask) .next(enableTrustedAccessForServicesTask) - .next(deployPhaseRolesTask) - .next(storePreviousOutput) - .next(deployPhase0Task) - .next(storePhase0Output) - .next(verifyFilesTask) - .next(enableConfigChoice); + .next(storeAllOutputsChoice); // Landing Zone Config Setup const alzConfigDefinition = loadLandingZoneConfigurationTask.startState @@ -823,8 +856,14 @@ export namespace InitialSetup { .next(cloudFormationMasterRoleChoice); const baseLineChoice = new sfn.Choice(this, 'Baseline?') - .when(sfn.Condition.stringEquals('$.configuration.baseline', 'LANDING_ZONE'), alzConfigDefinition) - .when(sfn.Condition.stringEquals('$.configuration.baseline', 'ORGANIZATIONS'), orgConfigDefinition) + .when( + sfn.Condition.stringEquals('$.configuration.baselineOutput.baseline', 'LANDING_ZONE'), + alzConfigDefinition, + ) + .when( + sfn.Condition.stringEquals('$.configuration.baselineOutput.baseline', 'ORGANIZATIONS'), + orgConfigDefinition, + ) .otherwise( new sfn.Fail(this, 'Fail', { cause: 'Invalid Baseline supplied', diff --git a/src/core/runtime/src/associate-hosted-zones-step.ts b/src/core/runtime/src/associate-hosted-zones-step.ts deleted file mode 100644 index 9760b7dd5..000000000 --- a/src/core/runtime/src/associate-hosted-zones-step.ts +++ /dev/null @@ -1,324 +0,0 @@ -import * as r53 from 'aws-sdk/clients/route53'; -import { DynamoDB } from '@aws-accelerator/common/src/aws/dynamodb'; -import { Account, getAccountId } from '@aws-accelerator/common-outputs/src/accounts'; -import { STS } from '@aws-accelerator/common/src/aws/sts'; -import { getStackJsonOutput, StackOutput, ResolversOutput } from '@aws-accelerator/common-outputs/src/stack-output'; -import { Route53 } from '@aws-accelerator/common/src/aws/route53'; -import { Route53Resolver } from '@aws-accelerator/common/src/aws/r53resolver'; -import { loadAcceleratorConfig } from '@aws-accelerator/common-config/src/load'; -import { LoadConfigurationInput } from './load-configuration-step'; -import { throttlingBackOff } from '@aws-accelerator/common/src/aws/backoff'; -import { VpcOutputFinder } from '@aws-accelerator/common-outputs/src/vpc'; -import { loadOutputs } from './utils/load-outputs'; -import { loadAccounts } from './utils/load-accounts'; - -interface AssociateHostedZonesInput extends LoadConfigurationInput { - assumeRoleName: string; - outputTableName: string; - parametersTableName: string; -} - -type ResolversOutputs = ResolversOutput[]; - -/** - * Auxiliary interface that represents a hosted zone in a specific account and VPC. - */ -interface AccountHostedZone { - accountKey: string; - accountId: string; - hostedZoneId: string; - associatedVpcIds: string[]; -} - -/** - * Auxiliary interface that represents a resolver rule in a specific account and VPC. - */ -interface AccountRule { - accountKey: string; - accountId: string; - ruleId: string; -} - -// Hosted zone ID is in the form of `/hostedzone/Z0181099DGX53XMU1D7S` -const hostedZoneIdRegex = /\/hostedzone\/([\d\w]+)/; - -const dynamodb = new DynamoDB(); -const sts = new STS(); - -export const handler = async (input: AssociateHostedZonesInput) => { - console.log(`Associating Hosted Zones with VPC...`); - console.log(JSON.stringify(input, null, 2)); - - const { - configRepositoryName, - assumeRoleName, - configCommitId, - configFilePath, - outputTableName, - parametersTableName, - } = input; - - const accounts = await loadAccounts(parametersTableName, dynamodb); - - // Retrieve Configuration from Code Commit with specific commitId - const config = await loadAcceleratorConfig({ - repositoryName: configRepositoryName, - filePath: configFilePath, - commitId: configCommitId, - }); - - const outputs = await loadOutputs(outputTableName, dynamodb); - - // get the private zones from global-options - const globalOptionsConfig = config['global-options']; - const privateZones = globalOptionsConfig.zones.names.private; - - const accountHostedZones: AccountHostedZone[] = []; - const accountRules: AccountRule[] = []; - for (const account of accounts) { - console.log(`Loading hosted zones for account ${account.key}`); - - // TODO Store all hosted zones in outputs and load those outputs here - // Find all hosted zones in Route53 - const credentials = await sts.getCredentialsForAccountAndRole(account.id, assumeRoleName); - const route53 = new Route53(credentials); - let listHostedZones; - let nextMarker; - do { - // TODO Use pagination withNextToken function when it supports NextMarker - listHostedZones = await route53.listHostedZones(undefined, nextMarker); - nextMarker = listHostedZones.NextMarker; - - const hostedZones = listHostedZones.HostedZones || []; - // get all private hosted zones - for (const hostedZone of hostedZones) { - if (!isPrivateHostedZone(privateZones, hostedZone)) { - continue; - } - - // Load already associated VPCs - const hostedZoneWithVpcs = await route53.getHostedZone(hostedZone.Id); - const associatedVpcs = hostedZoneWithVpcs.VPCs || []; - const associatedVpcIds = associatedVpcs.map(vpc => vpc.VPCId!); - - const match = hostedZone.Id.match(hostedZoneIdRegex); - if (!match) { - console.warn(`Cannot extract hosted zone ID from ${hostedZone.Id}`); - continue; - } - - const privateHostedZoneId = match[1]; - accountHostedZones.push({ - accountKey: account.key, - accountId: account.id, - hostedZoneId: privateHostedZoneId, - associatedVpcIds, - }); - } - } while (nextMarker); - - // Find all resolver rules in outputs - const resolversOutputs: ResolversOutputs[] = getStackJsonOutput(outputs, { - accountKey: account.key, - outputType: 'GlobalOptionsOutput', - }); - - const resolverOutputs = resolversOutputs.flatMap(list => list); - for (const resolverOutput of resolverOutputs) { - const inboundRuleId = resolverOutput.rules?.inBoundRule; - if (inboundRuleId) { - accountRules.push({ - accountKey: account.key, - accountId: account.id, - ruleId: inboundRuleId, - }); - } - resolverOutput.rules?.onPremRules?.forEach(ruleId => - accountRules.push({ - accountKey: account.key, - accountId: account.id, - ruleId, - }), - ); - } - - // TODO Merge above outputs and this one together - // Find all MAD resolver rules in outputs - // tslint:disable-next-line: no-any - const madRulesOutputs: any = getStackJsonOutput(outputs, { - accountKey: account.key, - outputType: 'MadRulesOutput', - }); - for (const madRulesOutput of madRulesOutputs) { - accountRules.push({ - accountKey: account.key, - accountId: account.id, - ruleId: madRulesOutput.Endpoint, - }); - } - } - - console.log('Starting association of private hosted zones with accounts VPC...'); - - for (const { accountKey, vpcConfig } of config.getVpcConfigs()) { - if (!vpcConfig['use-central-endpoints']) { - // TODO Disassociate hosted zones and resolver rules - continue; - } - - const accountId = getAccountId(accounts, accountKey); - if (!accountId) { - console.warn(`Cannot find account with accountKey ${accountKey}`); - continue; - } - - const vpcName = vpcConfig.name; - const vpcRegion = vpcConfig.region; - const vpcOutput = VpcOutputFinder.tryFindOneByAccountAndRegionAndName({ - outputs, - accountKey, - region: vpcRegion, - vpcName, - }); - if (!vpcOutput) { - console.warn(`Cannot find VPC "${vpcName}" in outputs`); - continue; - } - - const vpcId = vpcOutput.vpcId; - - // TODO Support the use-case that the VPC could have its own interface endpoints - - for (const accountHostedZone of accountHostedZones) { - if (accountHostedZone.associatedVpcIds.includes(vpcId)) { - console.log(`VPC ${vpcName} with ID ${vpcId} is already associated to PHZ ${accountHostedZone.hostedZoneId}`); - continue; - } - - await associateHostedZone({ - assumeRoleName, - vpcAccountId: accountId, - vpcName, - vpcId, - vpcRegion, - hostedZoneAccountId: accountHostedZone.accountId, - hostedZoneId: accountHostedZone.hostedZoneId, - }); - } - - for (const accountRule of accountRules) { - await associateResolverRule({ - assumeRoleName, - accountId, - resolverRuleId: accountRule.ruleId, - vpcId, - vpcName, - }); - } - } - - return { - status: 'SUCCESS', - statusReason: 'Associated Hosted Zones and resolver rules with the VPC', - }; -}; - -async function associateResolverRule(props: { - assumeRoleName: string; - accountId: string; - resolverRuleId: string; - vpcId: string; - vpcName?: string; -}) { - const { assumeRoleName, accountId, resolverRuleId, vpcId, vpcName } = props; - - const credentials = await sts.getCredentialsForAccountAndRole(accountId, assumeRoleName); - const r53Resolver = new Route53Resolver(credentials); - - try { - await throttlingBackOff(() => { - console.log(`Associating resolver rule ${resolverRuleId} with VPC ${vpcId} ${vpcName}...`); - return r53Resolver.associateResolverRule(resolverRuleId, vpcId); - }); - } catch (e) { - const message = `${e}`; - if (message.includes('Cannot associate rules with same domain name with same VPC')) { - // Domain already added; ignore this error and continue - } else { - // TODO Handle error - console.error(`Ignoring error while associating the resolver rule to VPC "${vpcName}"`); - console.error(message); - } - } -} - -/** - * Auxiliary function that associates the given VPC to the given hosted zone. An VPC association authorization is - * created when the VPC is in a different account than the hosted zone. - */ -async function associateHostedZone(props: { - assumeRoleName: string; - vpcAccountId: string; - vpcName?: string; - vpcId: string; - vpcRegion: string; - hostedZoneAccountId: string; - hostedZoneId: string; -}) { - const { assumeRoleName, vpcAccountId, vpcName, vpcId, vpcRegion, hostedZoneAccountId, hostedZoneId } = props; - - const vpcAccountCredentials = await sts.getCredentialsForAccountAndRole(vpcAccountId, assumeRoleName); - const vpcRoute53 = new Route53(vpcAccountCredentials); - - const hostedZoneAccountCredentials = await sts.getCredentialsForAccountAndRole(hostedZoneAccountId, assumeRoleName); - const hostedZoneRoute53 = new Route53(hostedZoneAccountCredentials); - - // authorize association of VPC with Hosted zones when VPC and Hosted Zones are defined in two different accounts - if (vpcAccountId !== hostedZoneAccountId) { - await throttlingBackOff(() => { - return hostedZoneRoute53.createVPCAssociationAuthorization(hostedZoneId, vpcId, vpcRegion); - }); - } - - // associate VPC with Hosted zones - try { - await throttlingBackOff(() => { - console.log(`Associating hosted zone ${hostedZoneId} with VPC ${vpcId} ${vpcName}...`); - return vpcRoute53.associateVPCWithHostedZone(hostedZoneId, vpcId, vpcRegion); - }); - } catch (e) { - if (e.code === 'ConflictingDomainExists') { - // 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); - } - } - - // delete association of VPC with Hosted zones when VPC and Hosted Zones are defined in two different accounts - if (vpcAccountId !== hostedZoneAccountId) { - await throttlingBackOff(() => { - return hostedZoneRoute53.deleteVPCAssociationAuthorization(hostedZoneId, vpcId, vpcRegion); - }); - } -} - -/** - * Returns true if the given hosted zone is in the given private zones or if is an interface or gateway endpoint. - */ -function isPrivateHostedZone(privateZones: string[], hostedZoned: r53.HostedZone): boolean { - // TODO need good logic to validate association with hosted zones, can be deprecated if we move to custom resources - if (hostedZoned.Name.includes('ca-central-1.amazonaws.com')) { - return true; - } else if (hostedZoned.Name.includes('notebook.ca-central-1.sagemaker.aws')) { - return true; - } else { - for (const privateZone of privateZones) { - if (hostedZoned.Name.includes(privateZone)) { - return true; - } - } - } - return false; -} diff --git a/src/core/runtime/src/get-baseline-step.ts b/src/core/runtime/src/get-baseline-step.ts index f7cd7ca7f..aaebcfb53 100644 --- a/src/core/runtime/src/get-baseline-step.ts +++ b/src/core/runtime/src/get-baseline-step.ts @@ -1,10 +1,11 @@ -import { arrayEqual } from '@aws-accelerator/common/src/util/arrays'; import { loadAcceleratorConfig } from '@aws-accelerator/common-config/src/load'; +import { DynamoDB } from '@aws-accelerator/common/src/aws/dynamodb'; -export interface LoadConfigurationInput { +export interface GetBaseLineInput { configFilePath: string; configRepositoryName: string; configCommitId: string; + outputTableName: string; acceleratorVersion?: string; } @@ -14,11 +15,19 @@ export interface ConfigurationOrganizationalUnit { ouName: string; } -export const handler = async (input: LoadConfigurationInput): Promise => { +export interface GetBaseelineOutput { + baseline: string; + storeAllOutputs: boolean; + phases: number[]; +} + +const dynamoDB = new DynamoDB(); + +export const handler = async (input: GetBaseLineInput): Promise => { console.log(`Loading configuration...`); console.log(JSON.stringify(input, null, 2)); - const { configFilePath, configRepositoryName, configCommitId } = input; + const { configFilePath, configRepositoryName, configCommitId, outputTableName } = input; // Retrieve Configuration from Code Commit with specific commitId const config = await loadAcceleratorConfig({ @@ -37,5 +46,12 @@ export const handler = async (input: LoadConfigurationInput): Promise => } else { throw new Error(`Both "alz-baseline" and "ct-baseline" can't be true`); } - return baseline; + + // Checking whether DynamoDB outputs table is empty or not + const storeAllOutputs = await dynamoDB.isEmpty(outputTableName); + return { + baseline, + storeAllOutputs, + phases: [-1, 0, 1, 2, 3], + }; }; diff --git a/src/core/runtime/src/index.ts b/src/core/runtime/src/index.ts index e51dbb036..20c6baa23 100644 --- a/src/core/runtime/src/index.ts +++ b/src/core/runtime/src/index.ts @@ -9,7 +9,6 @@ export { handler as loadAccountsStep } from './load-accounts-step'; export { handler as loadLandingZoneConfigurationStep } from './configuration/load-landing-zone-config'; export { handler as loadOrganizationConfigurationStep } from './configuration/load-organizations-config'; export { handler as loadLimitsStep } from './load-limits-step'; -export { handler as associateHostedZonesStep } from './associate-hosted-zones-step'; export { handler as accountDefaultSettingsStep } from './account-default-settings-step'; export { handler as storeStackOutputStep } from './store-stack-output-step'; export { handler as enableDirectorySharingStep } from './enable-directory-sharing-step'; diff --git a/src/core/runtime/src/load-configuration-step.ts b/src/core/runtime/src/load-configuration-step.ts index 96433135b..2d2ca0447 100644 --- a/src/core/runtime/src/load-configuration-step.ts +++ b/src/core/runtime/src/load-configuration-step.ts @@ -7,6 +7,8 @@ export interface LoadConfigurationInput { baseline?: BaseLineType; acceleratorVersion?: string; configRootFilePath?: string; + storeAllOutputs?: boolean; + phases?: number[]; } export interface LoadConfigurationOutput { diff --git a/src/deployments/cdk/package.json b/src/deployments/cdk/package.json index 383b8e132..6fb9e80da 100644 --- a/src/deployments/cdk/package.json +++ b/src/deployments/cdk/package.json @@ -40,6 +40,7 @@ "@aws-accelerator/custom-resource-accept-tgw-peering-attachment": "workspace:^0.0.1", "@aws-accelerator/custom-resource-acm-import-certificate": "workspace:^0.0.1", "@aws-accelerator/custom-resource-cfn-sleep": "workspace:^0.0.1", + "@aws-accelerator/custom-resource-cleanup": "workspace:^0.0.1", "@aws-accelerator/custom-resource-cloud-trail": "workspace:^0.0.1", "@aws-accelerator/custom-resource-create-tgw-peering-attachment": "workspace:^0.0.1", "@aws-accelerator/custom-resource-cur-report-definition": "workspace:^0.0.1", @@ -79,6 +80,9 @@ "@aws-accelerator/custom-resource-security-hub-send-invites": "workspace:^0.0.1", "@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 878450147..9c7fdaa73 100644 --- a/src/deployments/cdk/src/apps/phase--1.ts +++ b/src/deployments/cdk/src/apps/phase--1.ts @@ -85,4 +85,17 @@ export async function deploy({ acceleratorConfig, accountStacks, accounts }: Pha accountStacks, accounts, }); + + // Creates role for Resource cleanup custom resource + await globalRoles.createCleanupRoles({ + accountStacks, + accounts, + config: acceleratorConfig, + }); + + // Creates role for Resource cleanup custom resource + await globalRoles.createCentralEndpointDeploymentRole({ + accountStacks, + config: acceleratorConfig, + }); } diff --git a/src/deployments/cdk/src/apps/phase-0.ts b/src/deployments/cdk/src/apps/phase-0.ts index 952aa2f9e..d86e0d104 100644 --- a/src/deployments/cdk/src/apps/phase-0.ts +++ b/src/deployments/cdk/src/apps/phase-0.ts @@ -17,15 +17,7 @@ import * as passwordPolicy from '../deployments/iam-password-policy'; import * as transitGateway from '../deployments/transit-gateway'; import { getAccountId } from '../utils/accounts'; import * as rsyslogDeployment from '../deployments/rsyslog'; - -/********************************************************** - * DO NOT DEPEND ON OUTPUTS IN PHASE 0 * - * SINCE WE ARE CREATING CENTRAL BUCKET IN PHASE-0 * - * AND FRESH INSTALL WILL FAIL SINCE WE WILL NOT HAVE ANY * - * OUTPUTS CREATED IN PHASE -1 * - * (EXCEPT) ACCOUNTWARMING SINCE WE DON'T NEED OUTPUTS * - * ACCOUNTWARMING IN FIRST RUN * - **********************************************************/ +import * as cleanup from '../deployments/cleanup'; /** * This is the main entry point to deploy phase 0. @@ -178,6 +170,19 @@ export async function deploy({ acceleratorConfig, accountStacks, accounts, conte logBucket, }); + await cleanup.step1({ + accountStacks, + accounts, + config: acceleratorConfig, + outputs, + }); + + await cleanup.step2({ + accountStacks, + config: acceleratorConfig, + outputs, + }); + // TODO Deprecate these outputs const logArchiveAccountKey = acceleratorConfig['global-options']['central-log-services'].account; const logArchiveStack = accountStacks.getOrCreateAccountStack(logArchiveAccountKey); diff --git a/src/deployments/cdk/src/apps/phase-1.ts b/src/deployments/cdk/src/apps/phase-1.ts index 72e6b7184..6accb722b 100644 --- a/src/deployments/cdk/src/apps/phase-1.ts +++ b/src/deployments/cdk/src/apps/phase-1.ts @@ -35,10 +35,10 @@ import * as cwlCentralLoggingToS3 from '../deployments/central-services/central- import * as vpcDeployment from '../deployments/vpc'; import * as transitGateway from '../deployments/transit-gateway'; import { DNS_LOGGING_LOG_GROUP_REGION } from '@aws-accelerator/common/src/util/constants'; -import { createR53LogGroupName } from '../common/r53-zones'; import { LogGroup } from '@aws-accelerator/custom-resource-logs-log-group'; import { LogResourcePolicy } from '@aws-accelerator/custom-resource-logs-resource-policy'; import { IamRoleOutputFinder } from '@aws-accelerator/common-outputs/src/iam-role'; +import * as centralEndpoints from '../deployments/central-endpoints'; export interface IamPolicyArtifactsOutput { bucketArn: string; @@ -168,12 +168,22 @@ export async function deploy({ acceleratorConfig, accountStacks, accounts, conte endpointStack = new NestedStack(accountStack, `Endpoint${endpointStackIndex++}`); endpointCount = 0; } - new InterfaceEndpoint(endpointStack, pascalCase(endpoint), { + const interfaceEndpoint = new InterfaceEndpoint(endpointStack, pascalCase(endpoint), { serviceName: endpoint, vpcId: vpc.vpcId, vpcRegion: vpc.region, subnetIds, }); + + new centralEndpoints.CfnHostedZoneOutput(endpointStack, `HostedZoneOutput-${endpoint}`, { + accountKey, + domain: interfaceEndpoint.hostedZone.name, + hostedZoneId: interfaceEndpoint.hostedZone.ref, + region: vpc.region, + zoneType: 'PRIVATE', + serviceName: endpoint, + vpcName: vpc.name, + }); endpointCount++; } } @@ -467,8 +477,8 @@ export async function deploy({ acceleratorConfig, accountStacks, accounts, conte * Code to create LogGroups required for DNS Logging */ const globalOptionsConfig = acceleratorConfig['global-options']; - const zonesConfig = globalOptionsConfig.zones; - const zonesAccountKey = zonesConfig.account; + const zoneConfig = globalOptionsConfig.zones.find(zc => zc.names); + const zonesAccountKey = zoneConfig?.account!; const zonesStack = accountStacks.getOrCreateAccountStack(zonesAccountKey, DNS_LOGGING_LOG_GROUP_REGION); const logGroupLambdaRoleOutput = IamRoleOutputFinder.tryFindOneByName({ @@ -477,19 +487,20 @@ export async function deploy({ acceleratorConfig, accountStacks, accounts, conte roleKey: 'LogGroupRole', }); if (logGroupLambdaRoleOutput) { - const logGroups = zonesConfig.names.public.map(phz => { - const logGroupName = createR53LogGroupName({ - acceleratorPrefix: context.acceleratorPrefix, - domain: phz, - }); - return new LogGroup(zonesStack, `Route53HostedZoneLogGroup`, { - logGroupName, - roleArn: logGroupLambdaRoleOutput.roleArn, - }); - }); + const logGroups = + zoneConfig?.names?.public.map(phz => { + const logGroupName = centralEndpoints.createR53LogGroupName({ + acceleratorPrefix: context.acceleratorPrefix, + domain: phz, + }); + return new LogGroup(zonesStack, `Route53HostedZoneLogGroup`, { + logGroupName, + roleArn: logGroupLambdaRoleOutput.roleArn, + }); + }) || []; if (logGroups.length > 0) { - const wildcardLogGroupName = createR53LogGroupName({ + const wildcardLogGroupName = centralEndpoints.createR53LogGroupName({ acceleratorPrefix: context.acceleratorPrefix, domain: '*', }); diff --git a/src/deployments/cdk/src/apps/phase-3.ts b/src/deployments/cdk/src/apps/phase-3.ts index dbce7f83b..37a231478 100644 --- a/src/deployments/cdk/src/apps/phase-3.ts +++ b/src/deployments/cdk/src/apps/phase-3.ts @@ -1,7 +1,7 @@ import { PeeringConnection } from '../common/peering-connection'; -import { GlobalOptionsDeployment } from '../common/global-options'; import { PhaseInput } from './shared'; import * as alb from '../deployments/alb'; +import * as centralEndpoints from '../deployments/central-endpoints'; import * as rsyslogDeployment from '../deployments/rsyslog'; import { ImportedVpc } from '../deployments/vpc'; import { VpcOutput } from '@aws-accelerator/common-outputs/src/vpc'; @@ -36,24 +36,25 @@ export async function deploy({ acceleratorConfig, accountStacks, accounts, conte } /** - * Code to create DNS Resolvers + * CentralEndpoints.step1 creating public and private hosted zones in central account */ - const globalOptionsConfig = acceleratorConfig['global-options']; - const zonesConfig = globalOptionsConfig.zones; - const zonesAccountKey = zonesConfig.account; + await centralEndpoints.step1({ + acceleratorPrefix: context.acceleratorPrefix, + accountStacks, + config: acceleratorConfig, + outputs, + }); - // TODO Figure out how to keep the same logical IDs while supporting regions - const zonesStack = accountStacks.tryGetOrCreateAccountStack(zonesAccountKey); - if (!zonesStack) { - console.warn(`Cannot find account stack ${zonesAccountKey}`); - } else { - new GlobalOptionsDeployment(zonesStack, `GlobalOptionsDNSResolvers`, { - accounts, - outputs, - context, - acceleratorConfig, - }); - } + /** + * CentralEndpoints.step2 creating resolver endpoints and rules for on-premise & mad + * Share Central resolver rules to remote Accounts which has VPC in same region + */ + await centralEndpoints.step2({ + accountStacks, + config: acceleratorConfig, + outputs, + accounts, + }); await alb.step1({ accountStacks, diff --git a/src/deployments/cdk/src/apps/phase-4.ts b/src/deployments/cdk/src/apps/phase-4.ts index c3cec73e4..dc9f99cd9 100644 --- a/src/deployments/cdk/src/apps/phase-4.ts +++ b/src/deployments/cdk/src/apps/phase-4.ts @@ -1,13 +1,7 @@ -import * as cdk from '@aws-cdk/core'; -import { getAccountId } from '../utils/accounts'; -import { pascalCase } from 'pascal-case'; -import { getStackJsonOutput, ResolversOutput, MadRuleOutput } from '@aws-accelerator/common-outputs/src/stack-output'; -import { Route53ResolverRuleSharing } from '../common/r53-resolver-rule-sharing'; import { PhaseInput } from './shared'; import * as securityHub from '../deployments/security-hub'; import * as cloudWatchDeployment from '../deployments/cloud-watch'; - -type ResolversOutputs = ResolversOutput[]; +import * as centralEndpoints from '../deployments/central-endpoints'; export interface RdgwArtifactsOutput { accountKey: string; @@ -16,80 +10,7 @@ export interface RdgwArtifactsOutput { keyPrefix: string; } -export async function deploy({ acceleratorConfig, accounts, accountStacks, outputs }: PhaseInput) { - // to share the resolver rules - // get the list of account IDs with which the resolver rules needs to be shared - const vpcConfigs = acceleratorConfig.getVpcConfigs(); - const sharedAccountIds: string[] = []; - - const centralEndpointAccountKey = acceleratorConfig['global-options'].zones.account; - const hostedZonesAccountId = getAccountId(accounts, centralEndpointAccountKey); - for (const { accountKey, vpcConfig } of vpcConfigs) { - if (vpcConfig['use-central-endpoints']) { - const accountId = getAccountId(accounts, accountKey); - if (accountId && hostedZonesAccountId && accountId !== hostedZonesAccountId) { - sharedAccountIds.push(accountId); - } - } - } - - for (const { accountKey, vpcConfig } of vpcConfigs) { - const resolverRuleArns: string[] = []; - if (vpcConfig.resolvers) { - const accountId = getAccountId(accounts, accountKey); - - const resolversOutputs: ResolversOutputs[] = getStackJsonOutput(outputs, { - accountKey, - outputType: 'GlobalOptionsOutput', - }); - - for (const resolversOutput of resolversOutputs) { - const resolverOutput = resolversOutput.find(x => x.vpcName === vpcConfig.name); - if (!resolverOutput) { - console.warn( - `No Resolver Rules found in outputs for account key ${accountKey} and VPC name ${vpcConfig.name}`, - ); - continue; - } - - resolverRuleArns.push( - `arn:aws:route53resolver:${cdk.Aws.REGION}:${accountId}:resolver-rule/${resolverOutput.rules?.inBoundRule!}`, - ); - resolverOutput.rules?.onPremRules?.map(x => - resolverRuleArns.push(`arn:aws:route53resolver:${cdk.Aws.REGION}:${accountId}:resolver-rule/${x}`), - ); - } - - const madRulesOutputs: MadRuleOutput[] = getStackJsonOutput(outputs, { - accountKey, - outputType: 'MadRulesOutput', - }); - const madRulesOutput = madRulesOutputs.find(x => Object.keys(x)[0] === vpcConfig.name); - if (madRulesOutput) { - resolverRuleArns.push( - `arn:aws:route53resolver:${cdk.Aws.REGION}:${accountId}:resolver-rule/${madRulesOutput[vpcConfig.name]}`, - ); - } - - const r53ResolverRulesSharingStack = accountStacks.tryGetOrCreateAccountStack(accountKey); - if (!r53ResolverRulesSharingStack) { - console.warn(`Cannot find account stack ${accountKey}`); - continue; - } - - const route53ResolverRuleSharing = new Route53ResolverRuleSharing( - r53ResolverRulesSharingStack, - `ShareResolverRulesStack-${pascalCase(accountKey)}`, - { - name: 'PBMMAccel-Route53ResolverRulesSharing', - allowExternalPrincipals: false, - principals: sharedAccountIds, - resourceArns: resolverRuleArns, - }, - ); - } - } - +export async function deploy({ acceleratorConfig, accounts, accountStacks, outputs, context }: PhaseInput) { // Deploy Security Hub Step-3 to disable specific controls await securityHub.step3({ accountStacks, @@ -107,4 +28,25 @@ export async function deploy({ acceleratorConfig, accounts, accountStacks, outpu config: acceleratorConfig, outputs, }); + + /** + * Associate Shared Rules to VPC + */ + await centralEndpoints.step3({ + accountStacks, + config: acceleratorConfig, + outputs, + }); + + /** + * Associate Hosted Zones to VPC + */ + await centralEndpoints.step4({ + accountStacks, + config: acceleratorConfig, + outputs, + accounts, + executionRole: context.acceleratorPipelineRoleName, + assumeRole: context.acceleratorExecutionRoleName, + }); } diff --git a/src/deployments/cdk/src/apps/phase-5.ts b/src/deployments/cdk/src/apps/phase-5.ts index a365dada5..8eea66746 100644 --- a/src/deployments/cdk/src/apps/phase-5.ts +++ b/src/deployments/cdk/src/apps/phase-5.ts @@ -14,6 +14,7 @@ import * as cwlCentralLoggingToS3 from '../deployments/central-services/central- import { ArtifactOutputFinder } from '../deployments/artifacts/outputs'; import { ImageIdOutputFinder } from '@aws-accelerator/common-outputs/src/ami-output'; import * as cloudWatchDeployment from '../deployments/cloud-watch'; +import * as centralEndpoints from '../deployments/central-endpoints'; export async function deploy({ acceleratorConfig, accountStacks, accounts, context, outputs }: PhaseInput) { const accountNames = acceleratorConfig diff --git a/src/deployments/cdk/src/common/account-stacks.ts b/src/deployments/cdk/src/common/account-stacks.ts index 5aac46e49..66bea1e11 100644 --- a/src/deployments/cdk/src/common/account-stacks.ts +++ b/src/deployments/cdk/src/common/account-stacks.ts @@ -10,6 +10,7 @@ export interface AccountStackProps extends Omit { accountId: string; accountKey: string; region: string; + suffix?: string; } /** @@ -18,6 +19,7 @@ export interface AccountStackProps extends Omit { export class AccountStack extends AcceleratorStack { readonly accountId: string; readonly accountKey: string; + readonly suffix?: string; constructor(readonly app: cdk.Stage, id: string, props: AccountStackProps) { super(app, id, { @@ -30,6 +32,7 @@ export class AccountStack extends AcceleratorStack { this.accountId = props.accountId; this.accountKey = props.accountKey; + this.suffix = props.suffix; } } @@ -60,6 +63,10 @@ export class AccountApp extends cdk.Stage { get accountKey() { return this.stack.accountKey; } + + get suffix() { + return this.stack.suffix; + } } export interface AccountStacksProps { @@ -89,9 +96,11 @@ export class AccountStacks { /** * Get the existing stack for the given account or create a new stack if no such stack exists yet. */ - tryGetOrCreateAccountStack(accountKey: string, region?: string): AccountStack | undefined { + tryGetOrCreateAccountStack(accountKey: string, region?: string, suffix?: string): AccountStack | undefined { const regionOrDefault = region ?? this.props.context.defaultRegion; - const existingApp = this.apps.find(s => s.accountKey === accountKey && s.stack.region === regionOrDefault); + const existingApp = !suffix + ? this.apps.find(s => s.accountKey === accountKey && s.stack.region === regionOrDefault) + : this.apps.find(s => s.accountKey === accountKey && s.stack.region === regionOrDefault && s.suffix === suffix); if (existingApp) { return existingApp.stack; } @@ -101,8 +110,8 @@ export class AccountStacks { return undefined; } - const stackName = this.createStackName(accountKey, regionOrDefault); - const stackLogicalId = this.createStackLogicalId(accountKey, regionOrDefault); + const stackName = this.createStackName(accountKey, regionOrDefault, suffix); + const stackLogicalId = this.createStackLogicalId(accountKey, regionOrDefault, suffix); const terminationProtection = process.env.CONFIG_MODE === 'development' ? false : true; const outDir = this.props.useTempOutputDir ? tempy.directory() : undefined; @@ -116,25 +125,32 @@ export class AccountStacks { acceleratorPrefix: this.props.context.acceleratorPrefix, terminationProtection, region: regionOrDefault, + suffix, }, }); this.apps.push(app); return app.stack; } - protected createStackName(accountKey: string, region: string) { + protected createStackName(accountKey: string, region: string, suffix?: string) { // BE CAREFUL CHANGING THE STACK NAME // When changed, it will create a new stack and delete the old one! const accountPrettyName = pascalCase(accountKey).replace('_', ''); - return `${this.props.context.acceleratorPrefix}${accountPrettyName}-Phase${this.props.phase}`; + const suffixPretty = suffix ? pascalCase(suffix).replace('_', '') : ''; + return !suffix + ? `${this.props.context.acceleratorPrefix}${accountPrettyName}-Phase${this.props.phase}` + : `${this.props.context.acceleratorPrefix}${accountPrettyName}-Phase${this.props.phase}-${suffixPretty}`; } - protected createStackLogicalId(accountKey: string, region: string) { + protected createStackLogicalId(accountKey: string, region: string, suffix?: string) { // BE CAREFUL CHANGING THE STACK LOGICAL ID // When changed, it will generate new logical IDs for all resources in this stack and recreate all resources! const accountPrettyName = pascalCase(accountKey); + const suffixPretty = suffix ? pascalCase(suffix) : ''; const regionPrettyName = region === this.props.context.defaultRegion ? '' : pascalCase(region); - const stackConstructId = `${accountPrettyName}Phase${this.props.phase}${regionPrettyName}`; + const stackConstructId = !suffix + ? `${accountPrettyName}Phase${this.props.phase}${regionPrettyName}` + : `${accountPrettyName}Phase${this.props.phase}${regionPrettyName}${suffixPretty}`; return stackConstructId; } } diff --git a/src/deployments/cdk/src/common/global-options.ts b/src/deployments/cdk/src/common/global-options.ts deleted file mode 100644 index d8c832ed4..000000000 --- a/src/deployments/cdk/src/common/global-options.ts +++ /dev/null @@ -1,226 +0,0 @@ -import * as cdk from '@aws-cdk/core'; -import { AcceleratorConfig, VpcConfig } from '@aws-accelerator/common-config/src'; -import { VpcOutputFinder } from '@aws-accelerator/common-outputs/src/vpc'; -import { Account } from '../utils/accounts'; -import { Context } from '../utils/context'; -import { Route53Zones } from './r53-zones'; -import { Route53ResolverEndpoint } from './r53-resolver-endpoint'; -import { Route53ResolverRule } from './r53-resolver-rule'; -import { - StackOutput, - getStackJsonOutput, - MadRuleOutput, - ResolverRulesOutput, - ResolversOutput, -} from '@aws-accelerator/common-outputs/src/stack-output'; -import { JsonOutputValue } from './json-output'; - -export interface GlobalOptionsProps { - acceleratorConfig: AcceleratorConfig; - context: Context; - /** - * The accounts in the organization. - */ - accounts: Account[]; - /** - * Outputs - */ - outputs: StackOutput[]; -} - -/** - * Auxiliary construct that creates VPCs for organizational units. - */ -export class GlobalOptionsDeployment extends cdk.Construct { - constructor(scope: cdk.Construct, id: string, props: GlobalOptionsProps) { - super(scope, id); - - const { context, acceleratorConfig, outputs } = props; - - const vpcInBoundMapping = new Map(); - const vpcOutBoundMapping = new Map(); - - const zonesConfig = acceleratorConfig['global-options'].zones; - const zonesAccountKey = zonesConfig.account; - const zonesResolverVpcName = zonesConfig['resolver-vpc']; - - // Find the VPC in with the given name in the zones account - const resolverVpc = VpcOutputFinder.tryFindOneByAccountAndRegionAndName({ - outputs, - accountKey: zonesAccountKey, - vpcName: zonesResolverVpcName, - }); - if (!resolverVpc) { - console.warn(`Cannot find resolver VPC with name "${zonesResolverVpcName}"`); - return; - } - - // Creating Hosted Zones based on config - const r53Zones = new Route53Zones(this, 'DNSResolvers', { - zonesConfig, - vpcId: resolverVpc.vpcId, - vpcRegion: resolverVpc.region, - }); - - // Auxiliary method to create a resolvers in the account with given account key - const createResolvers = (accountKey: string, vpcConfig: VpcConfig): ResolversOutput | undefined => { - const resolversConfig = vpcConfig.resolvers; - if (!resolversConfig) { - console.warn(`Skipping resolver creation for VPC "${vpcConfig.name}" in account "${accountKey}"`); - return; - } - const vpcSubnet = vpcConfig.subnets?.find(s => s.name === resolversConfig.subnet); - if (!vpcSubnet) { - console.warn( - `Subnet provided in resolvers doesn't exist in Subnet = ${resolversConfig.subnet} and VPC = ${vpcConfig.name}`, - ); - return; - } - - const vpcOutput = VpcOutputFinder.tryFindOneByAccountAndRegionAndName({ - outputs, - accountKey, - region: vpcConfig.region, - vpcName: vpcConfig.name, - }); - if (!vpcOutput) { - console.warn(`Cannot find resolved VPC with name "${vpcConfig.name}"`); - return; - } - const subnetIds = vpcOutput.subnets.filter(s => s.subnetName === resolversConfig.subnet).map(s => s.subnetId); - if (subnetIds.length === 0) { - console.warn(`Cannot find subnet IDs for subnet name = ${resolversConfig.subnet} and VPC = ${vpcConfig.name}`); - return; - } - - // Call r53-resolver-endpoint per Account - const r53ResolverEndpoints = new Route53ResolverEndpoint(this, 'ResolverEndpoints', { - context, - vpcId: vpcOutput.vpcId, - name: vpcConfig.name, - subnetIds, - }); - const resolverOutput: ResolversOutput = { - vpcName: vpcConfig.name, - }; - const resolverRulesOutput: ResolverRulesOutput = {}; - - if (resolversConfig.inbound) { - r53ResolverEndpoints.enableInboundEndpoint(); - resolverOutput.inBound = r53ResolverEndpoints.inboundEndpointRef; - } - - if (resolversConfig.outbound) { - r53ResolverEndpoints.enableOutboundEndpoint(); - resolverOutput.outBound = r53ResolverEndpoints.outboundEndpointRef; - const onPremRules: string[] = []; - // 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 Route53ResolverRule(this, `${domainToName(onPremRuleConfig.zone)}-on-prem-phz-rule`, { - domain: onPremRuleConfig.zone, - endpoint: r53ResolverEndpoints.outboundEndpointRef, - ipAddresses: onPremRuleConfig['outbound-ips'], - ruleType: 'FORWARD', - name: `${domainToName(onPremRuleConfig.zone)}-phz-rule`, - vpcId: vpcOutput.vpcId, - }); - rule.node.addDependency(r53ResolverEndpoints); - onPremRules.push(rule.ruleId); - } - resolverRulesOutput.onPremRules = onPremRules; - } - - // Adding VPC Inbound Endpoint to Output - if (r53ResolverEndpoints.inboundEndpointRef) { - vpcInBoundMapping.set(vpcConfig.name, r53ResolverEndpoints.inboundEndpointRef); - } - - // Adding VPC Outbound Endpoint to Output - if (r53ResolverEndpoints.outboundEndpointRef) { - vpcOutBoundMapping.set(vpcConfig.name, r53ResolverEndpoints.outboundEndpointRef); - } - resolverOutput.rules = resolverRulesOutput; - return resolverOutput; - }; - - const resolverOutputs: ResolversOutput[] = []; - - // Create resolvers for all VPC configs - const vpcConfigs = acceleratorConfig.getVpcConfigs(); - for (const { ouKey, accountKey, vpcConfig } of vpcConfigs) { - console.debug( - `Deploying resolvers in account "${accountKey}"${ouKey ? ` and organizational unit "${ouKey}"` : ''}`, - ); - - const resolver = createResolvers(accountKey, vpcConfig); - if (resolver) { - resolverOutputs.push(resolver); - } - } - - const madRulesOutput: MadRuleOutput = {}; - // Check for MAD deployment, If already deployed then create Resolver Rule for MAD IPs - const accountConfigs = acceleratorConfig.getAccountConfigs(); - for (const [accountKey, accountConfig] of accountConfigs) { - const deploymentConfig = accountConfig.deployments; - if (!deploymentConfig || !deploymentConfig.mad) { - console.debug(`Skipping MAD deployment for account "${accountKey}"`); - continue; - } - - const madConfig = deploymentConfig.mad; - let madIPs: string[]; - const madOutput = getStackJsonOutput(outputs, { - accountKey, - outputType: 'MadOutput', - }); - if (madOutput.length === 0) { - console.warn(`MAD is not deployed yet in account ${accountKey}`); - continue; - } - madIPs = madOutput[0].dnsIps.split(','); - - const centralResolverAccount = madConfig['central-resolver-rule-account']; - const centralResolverVpcName = madConfig['central-resolver-rule-vpc']; - - const centralResolverVpc = VpcOutputFinder.tryFindOneByAccountAndRegionAndName({ - outputs, - accountKey: centralResolverAccount, - vpcName: centralResolverVpcName, - }); - if (!centralResolverVpc) { - console.warn(`Cannot find resolved VPC with name "${centralResolverVpcName}"`); - continue; - } - const endpointId = vpcOutBoundMapping.get(centralResolverVpcName); - if (!endpointId) { - console.warn(`Cannot find outbound mapping for VPC with name "${centralResolverVpcName}"`); - continue; - } - - const rule = new Route53ResolverRule(this, `${domainToName(madConfig['dns-domain'])}-phz-rule`, { - domain: madConfig['dns-domain'], - endpoint: endpointId, - ipAddresses: madIPs, - ruleType: 'FORWARD', - name: `${domainToName(madConfig['dns-domain'])}-mad-phz-rule`, - vpcId: resolverVpc.vpcId, - }); - madRulesOutput[centralResolverVpcName] = rule.ruleId; - } - - new JsonOutputValue(this, `GlobalOptionsOutput`, { - type: 'GlobalOptionsOutput', - value: resolverOutputs, - }); - - new JsonOutputValue(this, `MadRulesOutput`, { - type: 'MadRulesOutput', - value: madRulesOutput, - }); - } -} - -function domainToName(domain: string): string { - return domain.replace(/\./gi, '-'); -} diff --git a/src/deployments/cdk/src/common/interface-endpoints.ts b/src/deployments/cdk/src/common/interface-endpoints.ts index 5a7a7fa33..a0c40707f 100644 --- a/src/deployments/cdk/src/common/interface-endpoints.ts +++ b/src/deployments/cdk/src/common/interface-endpoints.ts @@ -14,6 +14,7 @@ export interface InterfaceEndpointProps { * SecurityGroup, VPCEndpoint, HostedZone and RecordSet. */ export class InterfaceEndpoint extends cdk.Construct { + private _hostedZone: route53.CfnHostedZone; constructor(scope: cdk.Construct, id: string, props: InterfaceEndpointProps) { super(scope, id); @@ -57,7 +58,7 @@ export class InterfaceEndpoint extends cdk.Construct { endpoint.addDependsOn(securityGroup); const hostedZoneName = zoneNameForRegionAndEndpointName(vpcRegion, serviceName); - const hostedZone = new route53.CfnHostedZone(this, 'Phz', { + this._hostedZone = new route53.CfnHostedZone(this, 'Phz', { name: hostedZoneName, vpcs: [ { @@ -69,15 +70,19 @@ export class InterfaceEndpoint extends cdk.Construct { comment: `zzEndpoint - ${serviceName}`, }, }); - hostedZone.addDependsOn(endpoint); + this._hostedZone.addDependsOn(endpoint); const recordSet = new route53.CfnRecordSet(this, 'RecordSet', { type: 'A', name: hostedZoneName, - hostedZoneId: hostedZone.ref, + hostedZoneId: this._hostedZone.ref, aliasTarget: aliasTargetForServiceNameAndEndpoint(serviceName, endpoint), }); - recordSet.addDependsOn(hostedZone); + recordSet.addDependsOn(this._hostedZone); + } + + get hostedZone(): route53.CfnHostedZone { + return this._hostedZone; } } diff --git a/src/deployments/cdk/src/common/r53-resolver-rule-sharing.ts b/src/deployments/cdk/src/common/r53-resolver-rule-sharing.ts deleted file mode 100644 index bb4b01016..000000000 --- a/src/deployments/cdk/src/common/r53-resolver-rule-sharing.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as cdk from '@aws-cdk/core'; -import * as ram from '@aws-cdk/aws-ram'; - -export interface Route53ResolverRuleSharingProps { - name: string; - allowExternalPrincipals: boolean; - principals: string[]; - resourceArns: string[]; -} - -/** - * Construct to share Route53 resolver rules using Resource Access Manager. - */ -export class Route53ResolverRuleSharing extends cdk.Construct { - constructor(parent: cdk.Construct, id: string, props: Route53ResolverRuleSharingProps) { - super(parent, id); - - // share the route53 resolver rules - new ram.CfnResourceShare(this, `Share-${props.name}`, { - name: props.name, - allowExternalPrincipals: props.allowExternalPrincipals, - principals: props.principals, - resourceArns: props.resourceArns, - }); - } -} diff --git a/src/deployments/cdk/src/common/r53-zones.ts b/src/deployments/cdk/src/common/r53-zones.ts deleted file mode 100644 index d7904ab18..000000000 --- a/src/deployments/cdk/src/common/r53-zones.ts +++ /dev/null @@ -1,72 +0,0 @@ -import * as cdk from '@aws-cdk/core'; -import * as r53 from '@aws-cdk/aws-route53'; - -import { GlobalOptionsZonesConfig } from '@aws-accelerator/common-config/src'; -import { DNS_LOGGING_LOG_GROUP_REGION } from '@aws-accelerator/common/src/util/constants'; -import { AcceleratorStack } from '@aws-accelerator/cdk-accelerator/src/core/accelerator-stack'; -import { trimSpecialCharacters } from '@aws-accelerator/common-outputs/src/secrets'; - -export interface Route53ZonesProps { - zonesConfig: GlobalOptionsZonesConfig; - vpcId: string; - vpcRegion: string; -} - -export class Route53Zones extends cdk.Construct { - readonly publicZoneToDomainMap = new Map(); - readonly privateZoneToDomainMap = new Map(); - - constructor(parent: cdk.Construct, name: string, props: Route53ZonesProps) { - super(parent, name); - - const stack = AcceleratorStack.of(this); - - const zoneConfig = props.zonesConfig; - const publicHostedZoneProps = zoneConfig.names.public; - const privateHostedZoneProps = zoneConfig.names.private; - - // Create Public Hosted Zones - for (const domain of publicHostedZoneProps) { - const logGroupName = createR53LogGroupName({ - acceleratorPrefix: stack.acceleratorPrefix, - domain, - }); - const logGroupArn = `arn:aws:logs:${DNS_LOGGING_LOG_GROUP_REGION}:${cdk.Aws.ACCOUNT_ID}:log-group:${logGroupName}`; - - const zone = new r53.CfnHostedZone(this, `${domain.replace('.', '-')}_pz`, { - name: domain, - hostedZoneConfig: { - comment: `PHZ - ${domain}`, - }, - queryLoggingConfig: { - cloudWatchLogsLogGroupArn: logGroupArn, - }, - }); - this.publicZoneToDomainMap.set(domain, zone.ref); - } - - // Form VPC Properties for Private Hosted Zone - const vpcProps: r53.CfnHostedZone.VPCProperty = { - vpcId: props.vpcId, - vpcRegion: props.vpcRegion, - }; - - // Create Private Hosted Zones - for (const domain of privateHostedZoneProps) { - const zone = new r53.CfnHostedZone(this, `${domain.replace('.', '-')}_pz`, { - name: domain, - vpcs: [vpcProps], - hostedZoneConfig: { - comment: `PHZ - ${domain}`, - }, - }); - this.privateZoneToDomainMap.set(domain, zone.ref); - } - } -} - -export function createR53LogGroupName(props: { acceleratorPrefix: string; domain: string }) { - const { acceleratorPrefix, domain } = props; - const prefix = trimSpecialCharacters(acceleratorPrefix); - return `/${prefix}/r53/${domain}`; -} diff --git a/src/deployments/cdk/src/deployments/central-endpoints/index.ts b/src/deployments/cdk/src/deployments/central-endpoints/index.ts new file mode 100644 index 000000000..198dc3e60 --- /dev/null +++ b/src/deployments/cdk/src/deployments/central-endpoints/index.ts @@ -0,0 +1,5 @@ +export * from './outputs'; +export * from './step-1'; +export * from './step-2'; +export * from './step-3'; +export * from './step-4'; diff --git a/src/deployments/cdk/src/deployments/central-endpoints/outputs.ts b/src/deployments/cdk/src/deployments/central-endpoints/outputs.ts new file mode 100644 index 000000000..839202576 --- /dev/null +++ b/src/deployments/cdk/src/deployments/central-endpoints/outputs.ts @@ -0,0 +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-1.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-1.ts new file mode 100644 index 000000000..e497edc6f --- /dev/null +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-1.ts @@ -0,0 +1,115 @@ +import * as c from '@aws-accelerator/common-config'; +import { AccountStacks } from '../../common/account-stacks'; +import * as cdk from '@aws-cdk/core'; +import { StackOutput } from '@aws-accelerator/common-outputs/src/stack-output'; +import { trimSpecialCharacters } from '@aws-accelerator/common-outputs/src/secrets'; +import { VpcOutputFinder } from '@aws-accelerator/common-outputs/src/vpc'; +import { DNS_LOGGING_LOG_GROUP_REGION } from '@aws-accelerator/common/src/util/constants'; +import * as r53 from '@aws-cdk/aws-route53'; +import { CfnHostedZoneOutput } from './outputs'; + +export interface CentralEndpointsStep1Props { + accountStacks: AccountStacks; + config: c.AcceleratorConfig; + outputs: StackOutput[]; + acceleratorPrefix: string; +} + +/** + * + * Create Hosted Zoens base on config from global-options/zones + * both public and private hosted zones + */ +export async function step1(props: CentralEndpointsStep1Props) { + const { accountStacks, config, outputs, acceleratorPrefix } = props; + const globalOptions = config['global-options']; + const zoneConfig = globalOptions.zones.find(zone => zone.names); + if (!zoneConfig) { + console.warn(`No configuration found under global-options/zones with names (public and private Hosted Zones)`); + return; + } + + const accountStack = accountStacks.tryGetOrCreateAccountStack(zoneConfig.account, zoneConfig.region); + if (!accountStack) { + console.error( + `Cannot find account stack ${zoneConfig.account}: ${zoneConfig.region}, while deploying Hosted Zones`, + ); + return; + } + + const publicHostedZones = zoneConfig.names?.public!; + const privateHostedZones = zoneConfig.names?.private!; + + // Create Public Hosted Zones + for (const domain of publicHostedZones) { + const logGroupName = createR53LogGroupName({ + acceleratorPrefix, + domain, + }); + const logGroupArn = `arn:aws:logs:${DNS_LOGGING_LOG_GROUP_REGION}:${cdk.Aws.ACCOUNT_ID}:log-group:${logGroupName}`; + const hostedZone = new r53.CfnHostedZone(accountStack, `${domain.replace('.', '-')}_pz`, { + name: domain, + hostedZoneConfig: { + comment: `PHZ - ${domain}`, + }, + queryLoggingConfig: { + cloudWatchLogsLogGroupArn: logGroupArn, + }, + }); + + new CfnHostedZoneOutput(accountStack, `HostedZoneOutput-${domain.replace('.', '-')}`, { + accountKey: zoneConfig.account, + domain, + hostedZoneId: hostedZone.ref, + region: zoneConfig.region, + zoneType: 'PUBLIC', + vpcName: undefined, + serviceName: undefined, + }); + } + + // Find the VPC in with the given name in the zones account + const resolverVpc = VpcOutputFinder.tryFindOneByAccountAndRegionAndName({ + outputs, + accountKey: zoneConfig.account, + vpcName: zoneConfig['resolver-vpc'], + region: zoneConfig.region, + }); + if (!resolverVpc) { + console.warn(`Cannot find resolver VPC with name "${zoneConfig['resolver-vpc']}"`); + return; + } + + // Form VPC Properties for Private Hosted Zone + const vpcProps: r53.CfnHostedZone.VPCProperty = { + vpcId: resolverVpc.vpcId, + vpcRegion: zoneConfig.region, + }; + + // Create Private Hosted Zones + for (const domain of privateHostedZones) { + const hostedZone = new r53.CfnHostedZone(accountStack, `${domain.replace('.', '-')}_pz`, { + name: domain, + vpcs: [vpcProps], + hostedZoneConfig: { + comment: `PHZ - ${domain}`, + }, + }); + + new CfnHostedZoneOutput(accountStack, `HostedZoneOutput-${domain.replace('.', '-')}`, { + accountKey: zoneConfig.account, + domain, + hostedZoneId: hostedZone.ref, + region: resolverVpc.region, + zoneType: 'PRIVATE', + vpcName: resolverVpc.vpcName, + serviceName: undefined, + }); + } +} + +export function createR53LogGroupName(props: { acceleratorPrefix: string; domain: string }) { + const { acceleratorPrefix, domain } = props; + const prefix = trimSpecialCharacters(acceleratorPrefix); + return `/${prefix}/r53/${domain}`; +} diff --git a/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts new file mode 100644 index 000000000..10186157a --- /dev/null +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-2.ts @@ -0,0 +1,398 @@ +import * as c from '@aws-accelerator/common-config'; +import * as cdk from '@aws-cdk/core'; +import { AccountStacks } from '../../common/account-stacks'; +import { + getStackJsonOutput, + ResolverRulesOutput, + ResolversOutput, + StackOutput, +} from '@aws-accelerator/common-outputs/src/stack-output'; +import { VpcOutputFinder } from '@aws-accelerator/common-outputs/src/vpc'; +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; + config: c.AcceleratorConfig; + outputs: StackOutput[]; + accounts: Account[]; +} + +/** + * Creates Route53 Resolver endpoints and resolver rules + * Inbound, OutBound Endoints + * Resolver Rules for on-Premise ips + * Resolver Rules for mad + */ +export async function step2(props: CentralEndpointsStep2Props) { + const { accountStacks, config, outputs, accounts } = props; + // Create resolvers for all VPC configs + const vpcConfigs = config.getVpcConfigs(); + 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) { + console.debug(`Skipping resolver creation for VPC "${vpcConfig.name}" in account "${accountKey}"`); + continue; + } + + /** + * Checking if current VPC is under Regional Central VPCs (global-options/zones), + * If yes the only we will share Rules from this account to another accounts + */ + const isRuleShareNeeded = !!zonesConfig.find( + zc => zc.account === accountKey && zc.region === vpcConfig.region && zc['resolver-vpc'] === vpcConfig.name, + ); + const vpcSubnet = vpcConfig.subnets?.find(s => s.name === resolversConfig.subnet); + if (!vpcSubnet) { + console.error( + `Subnet provided in resolvers doesn't exist in Subnet = ${resolversConfig.subnet} and VPC = ${vpcConfig.name}`, + ); + continue; + } + + const vpcOutput = VpcOutputFinder.tryFindOneByAccountAndRegionAndName({ + outputs, + accountKey, + region: vpcConfig.region, + vpcName: vpcConfig.name, + }); + if (!vpcOutput) { + console.error(`Cannot find resolved VPC with name "${vpcConfig.name}"`); + 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( + `Cannot find subnet IDs for subnet name = ${resolversConfig.subnet} and VPC = ${vpcConfig.name} in outputs`, + ); + continue; + } + + 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] = {}; + } + + // 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) { + console.error(`Cannot find account stack ${accountKey}: ${vpcConfig.region}, while deploying Resolver Endpoints`); + continue; + } + + // Call r53-resolver-endpoint per Account + const r53ResolverEndpoints = new ResolverEndpoint( + accountStack, + `${STACK_COMMON_SUFFIX}-${accountKey}-${vpcConfig.name}`, + { + vpcId: vpcOutput.vpcId, + name: vpcConfig.name, + subnetIds, + }, + ); + const resolverOutput: ResolversOutput = { + vpcName: vpcConfig.name, + accountKey, + region: vpcConfig.region, + }; + const resolverRulesOutput: ResolverRulesOutput = {}; + + if (resolversConfig.inbound) { + r53ResolverEndpoints.enableInboundEndpoint(); + resolverOutput.inBound = r53ResolverEndpoints.inboundEndpointRef; + } + const onPremRules: string[] = []; + const madRules: string[] = []; + if (resolversConfig.outbound) { + r53ResolverEndpoints.enableOutboundEndpoint(); + resolverOutput.outBound = r53ResolverEndpoints.outboundEndpointRef; + + // 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, + vpcId: vpcOutput.vpcId, + name: createRuleName(`${vpcConfig.name}-onprem-${domainToName(onPremRuleConfig.zone)}`), + }); + rule.node.addDependency(r53ResolverEndpoints.outboundEndpoint!); + onPremRules.push(rule.ruleId); + } + resolverRulesOutput.onPremRules = onPremRules; + + // Check for MAD configuration whose resolver is current account VPC + const madConfigsWithVpc = madConfigs.filter( + mc => + mc.mad['central-resolver-rule-account'] === accountKey && + mc.mad.region === vpcConfig.region && + mc.mad['central-resolver-rule-vpc'] === vpcConfig.name, + ); + for (const { accountKey: madAccountKey, mad } of madConfigsWithVpc) { + let madIPs: string[]; + const madOutput = getStackJsonOutput(outputs, { + accountKey: madAccountKey, + outputType: 'MadOutput', + }); + if (madOutput.length === 0) { + console.warn(`MAD is not deployed yet in account ${accountKey}`); + 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, + vpcId: vpcOutput.vpcId, + name: createRuleName(`${vpcConfig.name}-mad-${domainToName(mad['dns-domain'])}`), + }); + madRule.node.addDependency(r53ResolverEndpoints.outboundEndpoint!); + madRules.push(madRule.ruleId); + } + resolverRulesOutput.madRules = madRules; + } + resolverOutput.rules = resolverRulesOutput; + new JsonOutputValue(accountStack, `ResolverOutput-${resolverOutput.vpcName}`, { + type: 'GlobalOptionsOutput', + value: resolverOutput, + }); + + 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, + }); + } + } + + 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 (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; + } + } + } + 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 new file mode 100644 index 000000000..667b7087e --- /dev/null +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-3.ts @@ -0,0 +1,232 @@ +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 { 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; + config: c.AcceleratorConfig; + outputs: StackOutput[]; +} + +/** + * Associate VPC to Hosted Zones and Resoler Rules in central vpc account + */ +export async function step3(props: CentralEndpointsStep3Props) { + const { accountStacks, config, outputs } = props; + const allVpcConfigs = config.getVpcConfigs(); + + 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']) { + continue; + } + + // If Current VPC exists in global-options/zones then no need to share it with any Rules + if ( + accountKey === centralPhzConfig?.account && + vpcConfig.region === centralPhzConfig.region && + vpcConfig.name === centralPhzConfig['resolver-vpc'] + ) { + console.log( + `Current VPC Config ${accountKey}: ${vpcConfig.region}:${vpcConfig.name} is central VPC for Hosted Zones`, + ); + continue; + } + + // Retrieving current VPCId + const vpcOutput = VpcOutputFinder.tryFindOneByAccountAndRegionAndName({ + outputs, + accountKey, + region: vpcConfig.region, + vpcName: vpcConfig.name, + }); + if (!vpcOutput) { + console.error(`Cannot find resolved VPC with name "${vpcConfig.name}"`); + continue; + } + + const zoneConfig = config['global-options'].zones.find(z => z.region === vpcConfig.region); + if (!zoneConfig) { + console.error(`No Central VPC is defined in Region :: ${vpcConfig.region}`); + continue; + } + + const localCentralVpcConfig = config + .getVpcConfigs() + .find(vc => vc.accountKey === zoneConfig.account && vc.vpcConfig.name === zoneConfig['resolver-vpc']); + if (!localCentralVpcConfig) { + console.error( + `Central VPC Config is not found in Configuration under "global-options/zones": "${zoneConfig.account}: ${zoneConfig['resolver-vpc']}"`, + ); + continue; + } + + const resolversOutputs: ResolversOutput[] = getStackJsonOutput(outputs, { + accountKey: zoneConfig.account, + outputType: 'GlobalOptionsOutput', + }); + const resolverRegionoutputs = resolversOutputs.find( + resOut => resOut.region === vpcConfig.region && resOut.vpcName === centralPhzConfig?.['resolver-vpc'], + ); + if (!resolverRegionoutputs) { + console.error(`Resolver rules are not Deployed in Central VPC Region ${zoneConfig.account}::${vpcConfig.region}`); + continue; + } + + 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; + } + + 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) { + console.error(`Cannot find account stack ${accountKey}: ${vpcConfig.region}, while Associating Resolver Rules`); + continue; + } + + const roleOutput = IamRoleOutputFinder.tryFindOneByName({ + outputs, + accountKey, + roleKey: 'CentralEndpointDeployment', + }); + if (!roleOutput) { + continue; + } + + const ruleIds = [...resolverRegionoutputs.rules?.madRules!, ...resolverRegionoutputs.rules?.onPremRules!]; + 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 new file mode 100644 index 000000000..9adc6153f --- /dev/null +++ b/src/deployments/cdk/src/deployments/central-endpoints/step-4.ts @@ -0,0 +1,263 @@ +import { AccountStacks } from '../../common/account-stacks'; +import { StackOutput } from '@aws-accelerator/common-outputs/src/stack-output'; +import * as c from '@aws-accelerator/common-config'; +import { VpcOutputFinder } from '@aws-accelerator/common-outputs/src/vpc'; +import { HostedZoneOutputFinder } from '@aws-accelerator/common-outputs/src/hosted-zone'; +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; + config: c.AcceleratorConfig; + outputs: StackOutput[]; + accounts: Account[]; + executionRole: string; + assumeRole: 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 allVpcConfigs = config.getVpcConfigs(); + const zonesConfig = config['global-options'].zones; + const globalPrivateHostedZoneIds: string[] = []; + const centralZoneConfig = zonesConfig.find(z => z.names); + const masterAccountKey = config['global-options']['aws-org-master'].account; + if (centralZoneConfig) { + const hostedZoneOutputs = HostedZoneOutputFinder.findAll({ + outputs, + accountKey: centralZoneConfig.account, + region: centralZoneConfig.region, + }); + const centralVpcHostedZones = hostedZoneOutputs.filter(hzo => hzo.vpcName === centralZoneConfig['resolver-vpc']); + if (centralVpcHostedZones) { + globalPrivateHostedZoneIds.push( + ...centralVpcHostedZones + .filter(cvh => centralZoneConfig.names?.private.includes(cvh.domain)) + .map(hz => hz.hostedZoneId), + ); + } + } + + 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; + if (!vpcConfig['use-central-endpoints']) { + continue; + } + if ( + centralZoneConfig?.account === accountKey && + centralZoneConfig.region === vpcConfig.region && + centralZoneConfig['resolver-vpc'] === vpcConfig.name + ) { + console.info( + `Current VPC "${accountKey}: ${vpcConfig.region}: ${vpcConfig.name}" is Central VPC so no need to associate`, + ); + continue; + } + + const vpcOutput = VpcOutputFinder.tryFindOneByAccountAndRegionAndName({ + outputs, + accountKey, + region: vpcConfig.region, + vpcName: vpcConfig.name, + }); + if (!vpcOutput) { + console.warn(`Cannot find VPC "${vpcConfig.name}" in outputs`); + continue; + } + + 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; + } + + 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) { + console.error(`Cannot find account stack ${accountKey}: ${vpcConfig.region}, while Associating Resolver Rules`); + continue; + } + + const vpcAccountId = getAccountId(accounts, accountKey)!; + + const zoneConfig = zonesConfig.find(zc => zc.region === vpcConfig.region); + const hostedZoneIds: string[] = []; + if (zoneConfig) { + // Retriving Hosted Zone ids for interface endpoints to be shared + hostedZoneIds.push(...getHostedZoneIds(allVpcConfigs, zoneConfig, vpcConfig, outputs)); + if (zoneConfig.account === centralZoneConfig?.account) { + seperateGlobalHostedZonesAccount = false; + hostedZoneIds.push(...globalPrivateHostedZoneIds); + } + const hostedZoneAccountId = getAccountId(accounts, zoneConfig.account)!; + 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; + } + } + } + + 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); + } +} + +function getHostedZoneIds( + allVpcConfigs: c.ResolvedVpcConfig[], + zoneConfig: c.GlobalOptionsZonesConfig, + vpcConfig: c.VpcConfig, + outputs: StackOutput[], +): string[] { + // Retriving Hosted Zone ids for interface endpoints to be shared + const centralRegionalVpcConfig = allVpcConfigs.find( + vc => + vc.accountKey === zoneConfig.account && + vc.vpcConfig.name === zoneConfig['resolver-vpc'] && + vc.vpcConfig.region === zoneConfig.region, + ); + if (!centralRegionalVpcConfig) { + console.error( + `VPC configuration not found for Central configuraiton "${zoneConfig.account}: ${zoneConfig.region}: ${zoneConfig['resolver-vpc']}" `, + ); + return []; + } + const centralEndpoints: string[] = []; + const localEndpoints: string[] = []; + + // Get Endpoints from Central VPC Config + if (c.InterfaceEndpointConfig.is(centralRegionalVpcConfig.vpcConfig['interface-endpoints'])) { + centralEndpoints.push(...centralRegionalVpcConfig.vpcConfig['interface-endpoints'].endpoints); + } + + // Get Endpoints from Local VPC Config + if (c.InterfaceEndpointConfig.is(vpcConfig['interface-endpoints'])) { + localEndpoints.push(...vpcConfig['interface-endpoints'].endpoints); + } + const shareableEndpoints = centralEndpoints.filter(ce => !localEndpoints.includes(ce)); + const hostedZoneIds: string[] = []; + const regionalHostedZoneOutputs = HostedZoneOutputFinder.findAll({ + outputs, + accountKey: zoneConfig.account, + region: zoneConfig.region, + }); + const vpcHostedZoneOutputs = regionalHostedZoneOutputs.filter(hz => hz.vpcName === zoneConfig['resolver-vpc']); + hostedZoneIds.push( + ...vpcHostedZoneOutputs + .filter(hz => hz.serviceName && shareableEndpoints.includes(hz.serviceName)) + .map(h => h.hostedZoneId), + ); + return hostedZoneIds; +} diff --git a/src/deployments/cdk/src/deployments/cleanup/index.ts b/src/deployments/cdk/src/deployments/cleanup/index.ts new file mode 100644 index 000000000..229bbac20 --- /dev/null +++ b/src/deployments/cdk/src/deployments/cleanup/index.ts @@ -0,0 +1,2 @@ +export * from './step-1'; +export * from './step-2'; diff --git a/src/deployments/cdk/src/deployments/cleanup/outputs.ts b/src/deployments/cdk/src/deployments/cleanup/outputs.ts new file mode 100644 index 000000000..732915bc6 --- /dev/null +++ b/src/deployments/cdk/src/deployments/cleanup/outputs.ts @@ -0,0 +1,30 @@ +import * as t from 'io-ts'; +import { createCfnStructuredOutput } from '../../common/structured-output'; +import { createStructuredOutputFinder } from '@aws-accelerator/common-outputs/src/structured-output'; +import { StackOutput } from '@aws-accelerator/common-outputs/src/stack-output'; + +export const ResourceCleanupOutput = t.interface( + { + bucketPolicyCleanup: t.boolean, + }, + 'ResourceCleanupOutput', +); + +export type ResourceCleanupOutput = t.TypeOf; + +export const CfnResourceCleanupOutput = createCfnStructuredOutput(ResourceCleanupOutput); + +export const ResourceCleanupOutputFinder = createStructuredOutputFinder(ResourceCleanupOutput, finder => ({ + tryFindOneByName: (props: { + outputs: StackOutput[]; + accountKey?: string; + region?: string; + bucketPolicyCleanup?: boolean; + }) => + finder.tryFindOne({ + outputs: props.outputs, + accountKey: props.accountKey, + region: props.region, + predicate: r => r.bucketPolicyCleanup === props.bucketPolicyCleanup, + }), +})); diff --git a/src/deployments/cdk/src/deployments/cleanup/step-1.ts b/src/deployments/cdk/src/deployments/cleanup/step-1.ts new file mode 100644 index 000000000..4855269a2 --- /dev/null +++ b/src/deployments/cdk/src/deployments/cleanup/step-1.ts @@ -0,0 +1,73 @@ +import * as c from '@aws-accelerator/common-config/src'; +import { AccountStacks } from '../../common/account-stacks'; +import { StackOutput } from '@aws-accelerator/common-outputs/src/stack-output'; +import { IamRoleOutputFinder } from '@aws-accelerator/common-outputs/src/iam-role'; +import { ResourceCleanup } from '@aws-accelerator/custom-resource-cleanup'; +import { AccountBucketOutput } from '../defaults'; +import { Account } from '../../utils/accounts'; +import { ResourceCleanupOutputFinder } from './outputs'; + +export interface VpcFlowLogsBucketPermissionsCleanupProps { + accounts: Account[]; + accountStacks: AccountStacks; + config: c.AcceleratorConfig; + outputs: StackOutput[]; +} + +/** + * + * Delete VPC FlowLogs default S3 bucket permissions + * + */ +export async function step1(props: VpcFlowLogsBucketPermissionsCleanupProps) { + const { accounts, accountStacks, config, outputs } = props; + + // Finding the output for previous resource cleanup execution + const resourceCleanupOutput = ResourceCleanupOutputFinder.tryFindOneByName({ + outputs, + bucketPolicyCleanup: true, + }); + + // Checking if cleanup got executed in any of the previous SM runs + if (resourceCleanupOutput) { + return; + } + + // Find the account default buckets in the outputs + const accountBuckets = AccountBucketOutput.getAccountBuckets({ + accounts, + accountStacks, + config, + outputs, + }); + + const logArchiveAccount = config['global-options']['central-log-services'].account; + const securityAccount = config['global-options']['central-security-services'].account; + for (const accountKey of Object.keys(accountBuckets)) { + // Skip deletion of Log Archive and Security account default bucket policy + if (logArchiveAccount === accountKey || securityAccount === accountKey) { + console.log(`Skipping the deletion of bucket policy for account ${accountKey}`); + continue; + } + + const cleanupRoleOutput = IamRoleOutputFinder.tryFindOneByName({ + outputs, + accountKey, + roleKey: 'ResourceCleanupRole', + }); + if (!cleanupRoleOutput) { + continue; + } + + const accountStack = accountStacks.tryGetOrCreateAccountStack(accountKey); + if (!accountStack) { + console.warn(`Cannot find account stack ${accountKey}`); + continue; + } + + new ResourceCleanup(accountStack, `BucketPolicyCleanup${accountKey}`, { + bucketName: accountBuckets[accountKey].bucketName, + roleArn: cleanupRoleOutput.roleArn, + }); + } +} diff --git a/src/deployments/cdk/src/deployments/cleanup/step-2.ts b/src/deployments/cdk/src/deployments/cleanup/step-2.ts new file mode 100644 index 000000000..d93bd1f57 --- /dev/null +++ b/src/deployments/cdk/src/deployments/cleanup/step-2.ts @@ -0,0 +1,80 @@ +import * as c from '@aws-accelerator/common-config/src'; +import { AccountStacks } from '../../common/account-stacks'; +import { StackOutput } from '@aws-accelerator/common-outputs/src/stack-output'; +import { IamRoleOutputFinder } from '@aws-accelerator/common-outputs/src/iam-role'; +import { ResourceCleanup } from '@aws-accelerator/custom-resource-cleanup'; +import { ResourceCleanupOutputFinder } from './outputs'; + +export interface Route53CleanupProps { + accountStacks: AccountStacks; + config: c.AcceleratorConfig; + outputs: StackOutput[]; +} + +/** + * + * Deletes Route53 private hosted zones and resolver rules and its associations + * + */ +export async function step2(props: Route53CleanupProps) { + const { accountStacks, config, outputs } = props; + + // Finding the output for previous resource cleanup execution + const resourceCleanupOutput = ResourceCleanupOutputFinder.tryFindOneByName({ + outputs, + bucketPolicyCleanup: true, + }); + + // Checking if cleanup got executed in any of the previous SM runs + if (resourceCleanupOutput) { + console.warn(`Executed cleanup custom resource in the previous SM execution, skip calling cleanup custom resource`); + return; + } + + const centralVpcZoneConfig = config['global-options'].zones.find(zc => zc.names); + const centralZonesDomain: string[] = []; + if (centralVpcZoneConfig) { + centralZonesDomain.push(...(centralVpcZoneConfig.names?.private || [])); + } + const madConfigs = config.getMadConfigs(); + const madDomains = madConfigs.map(m => m.mad['dns-domain']); + + for (const { accountKey, vpcConfig } of config.getVpcConfigs()) { + const resolverRuleDomains: string[] = []; + const privateHostedZones: string[] = []; + + if (!vpcConfig.resolvers) { + continue; + } + + const rulesDomain: string[] = vpcConfig['on-premise-rules']?.map(r => r.zone) || []; + resolverRuleDomains.push(...rulesDomain, ...madDomains); + + resolverRuleDomains.push(...centralZonesDomain); + privateHostedZones.push(...centralZonesDomain.map(z => `${z}.`)); + + const cleanupRoleOutput = IamRoleOutputFinder.tryFindOneByName({ + outputs, + accountKey, + roleKey: 'ResourceCleanupRole', + }); + if (!cleanupRoleOutput) { + console.warn(`Cannot find Cleanup custom resource Roles output for account ${accountKey}`); + continue; + } + + const accountStack = accountStacks.tryGetOrCreateAccountStack(accountKey, vpcConfig.region); + if (!accountStack) { + console.warn(`Cannot find account stack ${accountKey}`); + continue; + } + + console.log('resolverRuleDomains', accountKey, resolverRuleDomains); + console.log('privateHostedZones', accountKey, privateHostedZones); + new ResourceCleanup(accountStack, `Route53Cleanup${accountKey}-${vpcConfig.name}-${vpcConfig.region}`, { + rulesDomainNames: resolverRuleDomains, + phzDomainNames: privateHostedZones, + roleArn: cleanupRoleOutput.roleArn, + }); + } +} diff --git a/src/deployments/cdk/src/deployments/defaults/step-1.ts b/src/deployments/cdk/src/deployments/defaults/step-1.ts index 0eda7568a..c6060f73d 100644 --- a/src/deployments/cdk/src/deployments/defaults/step-1.ts +++ b/src/deployments/cdk/src/deployments/defaults/step-1.ts @@ -231,7 +231,7 @@ function createCentralLogBucket(props: DefaultsStep1Props) { new iam.ServicePrincipal('cloudtrail.amazonaws.com'), new iam.ServicePrincipal('config.amazonaws.com'), ], - actions: ['s3:GetBucketAcl'], + actions: ['s3:GetBucketAcl', 's3:ListBucket'], resources: [`${logBucket.bucketArn}`], }), ); diff --git a/src/deployments/cdk/src/deployments/defaults/step-2.ts b/src/deployments/cdk/src/deployments/defaults/step-2.ts index fab442b82..159d46cc1 100644 --- a/src/deployments/cdk/src/deployments/defaults/step-2.ts +++ b/src/deployments/cdk/src/deployments/defaults/step-2.ts @@ -1,10 +1,12 @@ import * as cdk from '@aws-cdk/core'; import * as s3 from '@aws-cdk/aws-s3'; +import * as iam from '@aws-cdk/aws-iam'; import { AcceleratorConfig } from '@aws-accelerator/common-config/src'; import { AccountStacks } from '../../common/account-stacks'; import { Account, getAccountId } from '../../utils/accounts'; import { createDefaultS3Bucket, createDefaultS3Key } from './shared'; import { CfnAccountBucketOutput, AccountBuckets } from './outputs'; +import { CfnResourceCleanupOutput } from '../cleanup/outputs'; export interface DefaultsStep2Props { accountStacks: AccountStacks; @@ -56,6 +58,30 @@ function createDefaultS3Buckets(props: DefaultsStep2Props) { encryptionKey: key, logRetention, }); + + // Provide permissions to write VPC flow logs to the bucket + bucket.addToResourcePolicy( + new iam.PolicyStatement({ + principals: [new iam.ServicePrincipal('delivery.logs.amazonaws.com')], + actions: ['s3:PutObject'], + resources: [`${bucket.bucketArn}/${cdk.Aws.ACCOUNT_ID}/*`], + conditions: { + StringEquals: { + 's3:x-amz-acl': 'bucket-owner-full-control', + }, + }, + }), + ); + + // Provide permissions to read bucket for VPC flow logs + bucket.addToResourcePolicy( + new iam.PolicyStatement({ + principals: [new iam.ServicePrincipal('delivery.logs.amazonaws.com')], + actions: ['s3:GetBucketAcl', 's3:ListBucket'], + resources: [`${bucket.bucketArn}`], + }), + ); + bucket.replicateTo({ destinationBucket: centralLogBucket, destinationAccountId: logAccountId, @@ -71,5 +97,14 @@ function createDefaultS3Buckets(props: DefaultsStep2Props) { region: cdk.Aws.REGION, }); } + + // Finding master account key from the configuration + const masterAccountKey = config.getMandatoryAccountKey('master'); + const masterAccountStack = accountStacks.getOrCreateAccountStack(masterAccountKey); + // Writing to outputs to avoid future execution of Default bucket policy clean up custom resource + new CfnResourceCleanupOutput(masterAccountStack, `ResourceCleanupOutput${masterAccountKey}`, { + bucketPolicyCleanup: true, + }); + return buckets; } 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/cleanup-role.ts b/src/deployments/cdk/src/deployments/iam/cleanup-role.ts new file mode 100644 index 000000000..8e4e88b58 --- /dev/null +++ b/src/deployments/cdk/src/deployments/iam/cleanup-role.ts @@ -0,0 +1,69 @@ +import * as c from '@aws-accelerator/common-config/src'; +import * as iam from '@aws-cdk/aws-iam'; +import { AccountStacks, AccountStack } from '../../common/account-stacks'; +import { createIamRoleOutput } from './outputs'; +import { Account } from '@aws-accelerator/common-outputs/src/accounts'; + +export interface CleanupRoleProps { + accountStacks: AccountStacks; + accounts: Account[]; + config: c.AcceleratorConfig; +} + +export async function createCleanupRoles(props: CleanupRoleProps): Promise { + const { accountStacks, accounts, config } = props; + + const logArchiveAccount = config['global-options']['central-log-services'].account; + const securityAccount = config['global-options']['central-security-services'].account; + for (const account of accounts) { + // Added condition to skip creation of role in Log Archive and Security accounts + if (logArchiveAccount === account.key || securityAccount === account.key) { + console.log(`Skipping the creation of cleanup role for account ${account.key}`); + continue; + } + const accountStack = accountStacks.getOrCreateAccountStack(account.key); + const cleanupRole = await createResourceCleanupRole(accountStack); + createIamRoleOutput(accountStack, cleanupRole, 'ResourceCleanupRole'); + } +} + +export async function createResourceCleanupRole(stack: AccountStack) { + const role = new iam.Role(stack, 'Custom::ResourceCleanup', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + role.addToPrincipalPolicy( + new iam.PolicyStatement({ + actions: ['s3:DeleteBucketPolicy'], + resources: ['*'], + }), + ); + + role.addToPrincipalPolicy( + new iam.PolicyStatement({ + actions: [ + 'route53resolver:ListResolverRules', + 'route53resolver:ListResolverRuleAssociations', + 'route53resolver:DisassociateResolverRule', + 'route53resolver:DeleteResolverRule', + 'ec2:DescribeVpcs', + ], + resources: ['*'], + }), + ); + + role.addToPrincipalPolicy( + new iam.PolicyStatement({ + actions: ['route53:ListHostedZonesByName', 'route53:DeleteHostedZone'], + 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 be55b877a..252a4ea6a 100644 --- a/src/deployments/cdk/src/deployments/iam/index.ts +++ b/src/deployments/cdk/src/deployments/iam/index.ts @@ -13,3 +13,5 @@ export * from './tgw-create-peering-roles'; 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 bc6c5a7b6..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 @@ -226,6 +226,8 @@ 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: MasterPhase3 1`] = `Array []`; +exports[`there should not be any unsupported resource changes for AWS::Budgets::Budget: MasterPhase4HostedZonesAssc1 1`] = `Array []`; + exports[`there should not be any unsupported resource changes for AWS::Budgets::Budget: MasterPhase5 1`] = `Array []`; exports[`there should not be any unsupported resource changes for AWS::Budgets::Budget: MasterPhase5UsEast1 1`] = `Array []`; @@ -628,7 +630,7 @@ 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: SharedNetworkPhase4 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 []`; @@ -664,6 +666,8 @@ 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: MasterPhase3 1`] = `Array []`; +exports[`there should not be any unsupported resource changes for AWS::DirectoryService::MicrosoftAD: MasterPhase4HostedZonesAssc1 1`] = `Array []`; + exports[`there should not be any unsupported resource changes for AWS::DirectoryService::MicrosoftAD: MasterPhase5 1`] = `Array []`; exports[`there should not be any unsupported resource changes for AWS::DirectoryService::MicrosoftAD: MasterPhase5UsEast1 1`] = `Array []`; @@ -771,7 +775,7 @@ 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: SharedNetworkPhase4 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 []`; @@ -807,6 +811,8 @@ 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: MasterPhase3 1`] = `Array []`; +exports[`there should not be any unsupported resource changes for AWS::EC2::Instance: MasterPhase4HostedZonesAssc1 1`] = `Array []`; + exports[`there should not be any unsupported resource changes for AWS::EC2::Instance: MasterPhase5 1`] = `Array []`; exports[`there should not be any unsupported resource changes for AWS::EC2::Instance: MasterPhase5UsEast1 1`] = `Array []`; @@ -1034,7 +1040,7 @@ 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: SharedNetworkPhase4 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 []`; @@ -1070,6 +1076,8 @@ 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: MasterPhase3 1`] = `Array []`; +exports[`there should not be any unsupported resource changes for AWS::EC2::TransitGateway: MasterPhase4HostedZonesAssc1 1`] = `Array []`; + exports[`there should not be any unsupported resource changes for AWS::EC2::TransitGateway: MasterPhase5 1`] = `Array []`; exports[`there should not be any unsupported resource changes for AWS::EC2::TransitGateway: MasterPhase5UsEast1 1`] = `Array []`; @@ -1167,7 +1175,7 @@ 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: SharedNetworkPhase4 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 []`; @@ -1203,6 +1211,8 @@ 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: MasterPhase3 1`] = `Array []`; +exports[`there should not be any unsupported resource changes for AWS::ElasticLoadBalancingV2::LoadBalancer: MasterPhase4HostedZonesAssc1 1`] = `Array []`; + exports[`there should not be any unsupported resource changes for AWS::ElasticLoadBalancingV2::LoadBalancer: MasterPhase5 1`] = `Array []`; exports[`there should not be any unsupported resource changes for AWS::ElasticLoadBalancingV2::LoadBalancer: MasterPhase5UsEast1 1`] = `Array []`; @@ -1296,7 +1306,7 @@ 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: SharedNetworkPhase4 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 []`; @@ -1379,6 +1389,8 @@ 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: MasterPhase3 1`] = `Array []`; +exports[`there should not be any unsupported resource changes for AWS::S3::Bucket: MasterPhase4HostedZonesAssc1 1`] = `Array []`; + exports[`there should not be any unsupported resource changes for AWS::S3::Bucket: MasterPhase5 1`] = `Array []`; exports[`there should not be any unsupported resource changes for AWS::S3::Bucket: MasterPhase5UsEast1 1`] = `Array []`; @@ -1501,7 +1513,7 @@ 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: SharedNetworkPhase4 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 []`; @@ -1604,6 +1616,8 @@ 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: MasterPhase3 1`] = `Array []`; +exports[`there should not be any unsupported resource changes for AWS::SecretsManager::ResourcePolicy: MasterPhase4HostedZonesAssc1 1`] = `Array []`; + exports[`there should not be any unsupported resource changes for AWS::SecretsManager::ResourcePolicy: MasterPhase5 1`] = `Array []`; exports[`there should not be any unsupported resource changes for AWS::SecretsManager::ResourcePolicy: MasterPhase5UsEast1 1`] = `Array []`; @@ -1676,7 +1690,7 @@ 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: SharedNetworkPhase4 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 []`; @@ -1763,6 +1777,8 @@ 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: MasterPhase3 1`] = `Array []`; +exports[`there should not be any unsupported resource changes for AWS::SecretsManager::Secret: MasterPhase4HostedZonesAssc1 1`] = `Array []`; + exports[`there should not be any unsupported resource changes for AWS::SecretsManager::Secret: MasterPhase5 1`] = `Array []`; exports[`there should not be any unsupported resource changes for AWS::SecretsManager::Secret: MasterPhase5UsEast1 1`] = `Array []`; @@ -1853,7 +1869,7 @@ 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: SharedNetworkPhase4 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/package.json b/src/lib/cdk-constructs/package.json index 4ed7b9121..1c35379d4 100644 --- a/src/lib/cdk-constructs/package.json +++ b/src/lib/cdk-constructs/package.json @@ -40,12 +40,14 @@ "@aws-cdk/aws-ssm": "1.46.0", "@aws-cdk/core": "1.46.0", "@aws-cdk/aws-securityhub": "1.46.0", + "@aws-cdk/aws-route53resolver": "1.46.0", "@aws-accelerator/custom-resource-cfn-sleep": "workspace:^0.0.1", "@aws-accelerator/custom-resource-ec2-keypair": "workspace:^0.0.1", "@aws-accelerator/custom-resource-s3-template": "workspace:^0.0.1", "@aws-accelerator/custom-resource-security-hub-accept-invites": "workspace:^0.0.1", "@aws-accelerator/custom-resource-security-hub-enable": "workspace:^0.0.1", "@aws-accelerator/custom-resource-security-hub-send-invites": "workspace:^0.0.1", + "@aws-accelerator/custom-resource-r53-dns-endpoint-ips": "workspace:^0.0.1", "hash-sum": "^2.0.0", "ip-num": "^1.2.2", "pascal-case": "^3.1.1", diff --git a/src/lib/cdk-constructs/src/route53/index.ts b/src/lib/cdk-constructs/src/route53/index.ts new file mode 100644 index 000000000..220c0128c --- /dev/null +++ b/src/lib/cdk-constructs/src/route53/index.ts @@ -0,0 +1,2 @@ +export * from './resolver-endpoint'; +export * from './resolver-rule'; diff --git a/src/deployments/cdk/src/common/r53-resolver-endpoint.ts b/src/lib/cdk-constructs/src/route53/resolver-endpoint.ts similarity index 76% rename from src/deployments/cdk/src/common/r53-resolver-endpoint.ts rename to src/lib/cdk-constructs/src/route53/resolver-endpoint.ts index 3c08956bb..099e7a166 100644 --- a/src/deployments/cdk/src/common/r53-resolver-endpoint.ts +++ b/src/lib/cdk-constructs/src/route53/resolver-endpoint.ts @@ -1,11 +1,9 @@ import * as cdk from '@aws-cdk/core'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as r53resolver from '@aws-cdk/aws-route53resolver'; -import { Context } from '../utils/context'; import { R53DnsEndpointIps } from '@aws-accelerator/custom-resource-r53-dns-endpoint-ips'; -export interface Route53ResolverEndpointProps { - context: Context; +export interface ResolverEndpointProps { /** * The name that will be added to the description of the endpoint resolvers. */ @@ -20,12 +18,12 @@ export interface Route53ResolverEndpointProps { subnetIds: string[]; } -export class Route53ResolverEndpoint extends cdk.Construct { +export class ResolverEndpoint extends cdk.Construct { private _inboundEndpoint: r53resolver.CfnResolverEndpoint | undefined; private _outboundEndpoint: r53resolver.CfnResolverEndpoint | undefined; - private _inboundEndpointIps: string[] = []; + // private _inboundEndpointIps: string[] = []; - constructor(parent: cdk.Construct, id: string, private readonly props: Route53ResolverEndpointProps) { + constructor(parent: cdk.Construct, id: string, private readonly props: ResolverEndpointProps) { super(parent, id); } @@ -41,7 +39,7 @@ export class Route53ResolverEndpoint extends cdk.Construct { const securityGroup = new ec2.CfnSecurityGroup(this, `InboundSecurityGroup`, { groupDescription: 'Security Group for Public Hosted Zone Inbound EndpointRoute53', vpcId: this.props.vpcId, - groupName: `${this.props.name}_inbound_sg`, + groupName: `${this.props.name}_inbound_endpoint_sg`, }); const ipAddresses = this.props.subnetIds.map(subnetId => ({ @@ -55,13 +53,14 @@ export class Route53ResolverEndpoint 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, - }); + // const dnsIps = new R53DnsEndpointIps(this, 'InboundIp', { + // resolverEndpointId: this._inboundEndpoint.ref, + // }); - // Every IP address that we supply to inbound endpoint will result in an DNS endpoint IP - this._inboundEndpointIps = ipAddresses.map((_, index) => dnsIps.getEndpointIpAddress(index)); + // // Every IP address that we supply to inbound endpoint will result in an DNS endpoint IP + // this._inboundEndpointIps = ipAddresses.map((_, index) => dnsIps.getEndpointIpAddress(index)); return this._inboundEndpoint; } @@ -78,7 +77,7 @@ export class Route53ResolverEndpoint extends cdk.Construct { const securityGroup = new ec2.CfnSecurityGroup(this, `OutboundSecurityGroup`, { groupDescription: 'Security Group for Public Hosted Zone Outbound EndpointRoute53', vpcId: this.props.vpcId, - groupName: `${this.props.name}_outbound_sg`, + groupName: `${this.props.name}_outbound_endpoint_sg`, }); const ipAddresses = this.props.subnetIds.map(subnetId => ({ @@ -92,6 +91,7 @@ export class Route53ResolverEndpoint extends cdk.Construct { securityGroupIds: [securityGroup.ref], name: `${this.props.name} Outbound Endpoint`, }); + this._outboundEndpoint.addDependsOn(securityGroup); return this._outboundEndpoint; } @@ -111,8 +111,8 @@ export class Route53ResolverEndpoint extends cdk.Construct { return this.outboundEndpoint?.ref; } - get inboundEndpointIps(): string[] { - // Return a copy of the list so the original one isn't mutable - return [...this._inboundEndpointIps]; - } + // get inboundEndpointIps(): string[] { + // // Return a copy of the list so the original one isn't mutable + // return [...this._inboundEndpointIps]; + // } } diff --git a/src/deployments/cdk/src/common/r53-resolver-rule.ts b/src/lib/cdk-constructs/src/route53/resolver-rule.ts similarity index 83% rename from src/deployments/cdk/src/common/r53-resolver-rule.ts rename to src/lib/cdk-constructs/src/route53/resolver-rule.ts index 7787126f2..4fe4598d0 100644 --- a/src/deployments/cdk/src/common/r53-resolver-rule.ts +++ b/src/lib/cdk-constructs/src/route53/resolver-rule.ts @@ -1,7 +1,7 @@ import * as cdk from '@aws-cdk/core'; import * as r53Resolver from '@aws-cdk/aws-route53resolver'; -export interface Route53ResolverRuleProps { +export interface ResolverRuleProps { vpcId: string; name: string; domain: string; @@ -13,10 +13,10 @@ export interface Route53ResolverRuleProps { /** * Auxiliary construct that creates a Route53 resolver rule for the and associates it with the given VPC. */ -export class Route53ResolverRule extends cdk.Construct { +export class ResolverRule extends cdk.Construct { private readonly rule: r53Resolver.CfnResolverRule; - constructor(parent: cdk.Construct, id: string, props: Route53ResolverRuleProps) { + constructor(parent: cdk.Construct, id: string, props: ResolverRuleProps) { super(parent, id); const targetIps = props.ipAddresses.map(ip => ({ ip, diff --git a/src/lib/common-config/src/index.ts b/src/lib/common-config/src/index.ts index 416f5dc41..9eacfd969 100644 --- a/src/lib/common-config/src/index.ts +++ b/src/lib/common-config/src/index.ts @@ -592,7 +592,8 @@ export const ZoneNamesConfigType = t.interface({ export const GlobalOptionsZonesConfigType = t.interface({ account: NonEmptyString, 'resolver-vpc': NonEmptyString, - names: ZoneNamesConfigType, + names: optional(ZoneNamesConfigType), + region: NonEmptyString, }); export const CostAndUsageReportConfigType = t.interface({ @@ -744,7 +745,7 @@ export const GlobalOptionsConfigType = t.interface({ 'default-s3-retention': t.number, 'central-bucket': NonEmptyString, reports: ReportsConfigType, - zones: GlobalOptionsZonesConfigType, + zones: t.array(GlobalOptionsZonesConfigType), 'security-hub-frameworks': SecurityHubFrameworksConfigType, 'central-security-services': CentralServicesConfigType, 'central-operations-services': CentralServicesConfigType, @@ -840,6 +841,13 @@ export interface ResolvedAlbConfig extends ResolvedConfigBase { albs: AlbConfig[]; } +export interface ResolvedMadConfig extends ResolvedConfigBase { + /** + * The mad config to be deployed. + */ + mad: MadDeploymentConfig; +} + export class AcceleratorConfig implements t.TypeOf { readonly 'global-options': GlobalOptionsConfig; readonly 'mandatory-account-configs': AccountsConfig; @@ -1112,6 +1120,24 @@ export class AcceleratorConfig implements t.TypeOf return result; } + /** + * Find all mad configurations in mandatory accounts, workload accounts and organizational units. + */ + getMadConfigs(): ResolvedMadConfig[] { + const result: ResolvedMadConfig[] = []; + for (const [key, config] of this.getAccountConfigs()) { + const mad = config.deployments?.mad; + if (!mad) { + continue; + } + result.push({ + accountKey: key, + mad, + }); + } + return result; + } + /** * Iterate all account configs and organizational unit configs in order. */ diff --git a/src/lib/common-outputs/src/hosted-zone.ts b/src/lib/common-outputs/src/hosted-zone.ts new file mode 100644 index 000000000..57555a66b --- /dev/null +++ b/src/lib/common-outputs/src/hosted-zone.ts @@ -0,0 +1,39 @@ +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 HOSTEDZONE = ['PUBLIC', 'PRIVATE'] as const; + +export const HostedZoneType = enumType(HOSTEDZONE, 'HostedZoneType'); + +export const HostedZoneOutput = t.interface( + { + accountKey: t.string, + region: t.string, + hostedZoneId: t.string, + domain: t.string, + zoneType: HostedZoneType, + serviceName: optional(t.string), + vpcName: optional(t.string), + }, + 'HostedZoneOutput', +); + +export type HostedZoneOutput = t.TypeOf; + +export const HostedZoneOutputFinder = createStructuredOutputFinder(HostedZoneOutput, finder => ({ + tryFindOneByAccountAndRegionAndType: (props: { + outputs: StackOutput[]; + accountKey?: string; + region?: string; + zoneType?: string; + }) => + finder.tryFindOne({ + outputs: props.outputs, + predicate: output => + (props.accountKey === undefined || output.accountKey === props.accountKey) && + (props.region === undefined || output.region === props.region) && + (props.zoneType === undefined || output.zoneType === props.zoneType), + }), +})); diff --git a/src/lib/common-outputs/src/stack-output.ts b/src/lib/common-outputs/src/stack-output.ts index 241acfeab..c7b1d8bfb 100644 --- a/src/lib/common-outputs/src/stack-output.ts +++ b/src/lib/common-outputs/src/stack-output.ts @@ -72,11 +72,13 @@ export interface MadRuleOutput { export interface ResolverRulesOutput { onPremRules?: string[]; inBoundRule?: string; - madRules?: MadRuleOutput; + madRules?: string[]; } export interface ResolversOutput { vpcName: string; + accountKey: string; + region: string; inBound?: string; outBound?: string; rules?: ResolverRulesOutput; 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 0a36a170a..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; @@ -32,6 +33,18 @@ export class DynamoDB { return items; } + async isEmpty(tableName: string): Promise { + const record = await throttlingBackOff(() => + this.client + .scan({ + TableName: tableName, + Limit: 1, + }) + .promise(), + ); + return !record.Count; + } + async putItem(props: dynamodb.PutItemInput): Promise { await throttlingBackOff(() => this.client.putItem(props).promise()); } diff --git a/src/lib/custom-resources/cdk-associate-hosted-zones/README.md b/src/lib/custom-resources/cdk-associate-hosted-zones/README.md new file mode 100644 index 000000000..e3305d8be --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-hosted-zones/README.md @@ -0,0 +1,18 @@ +# Associate Hosted Zones to VPC + +This is a custom resource to associate vpc to Hosted Zone Used `createVPCAssociationAuthorization`, `associateVPCWithHostedZone`, `deleteVPCAssociationAuthorization` and `deleteVPCAssociationAuthorization` API calls. + +## Usage + + import { AssociateHostedZones } from '@aws-accelerator/custom-resource-associate-hosted-zones'; + + 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/cdk/index.ts b/src/lib/custom-resources/cdk-associate-hosted-zones/cdk/index.ts new file mode 100644 index 000000000..b7c146276 --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-hosted-zones/cdk/index.ts @@ -0,0 +1,60 @@ +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::AssociateHostedZones'; + +export interface AssociateHostedZonesProps { + assumeRoleName: string; + vpcAccountId: string; + vpcName: string; + vpcId: string; + vpcRegion: string; + hostedZoneAccountId: string; + hostedZoneIds: string[]; + roleArn: string; +} + +export interface AssociateHostedZonesRuntimeProps extends Omit {} +/** + * Custom resource that will create SSM Document. + */ +export class AssociateHostedZones extends cdk.Construct { + private readonly resource: cdk.CustomResource; + private role: iam.IRole; + + constructor(scope: cdk.Construct, id: string, props: AssociateHostedZonesProps) { + super(scope, id); + this.role = iam.Role.fromRoleArn(this, `${resourceType}Role`, props.roleArn); + + const runtimeProps: AssociateHostedZonesRuntimeProps = 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-hosted-zones-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-hosted-zones/package.json b/src/lib/custom-resources/cdk-associate-hosted-zones/package.json new file mode 100644 index 000000000..d51dd1720 --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-hosted-zones/package.json @@ -0,0 +1,24 @@ +{ + "name": "@aws-accelerator/custom-resource-associate-hosted-zones", + "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-hosted-zones-runtime": "workspace:^0.0.1" + } +} \ No newline at end of file diff --git a/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/.gitignore b/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/.gitignore new file mode 100644 index 000000000..1521c8b76 --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/.gitignore @@ -0,0 +1 @@ +dist diff --git a/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/package.json b/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/package.json new file mode 100644 index 000000000..3212928bf --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/package.json @@ -0,0 +1,31 @@ +{ + "name": "@aws-accelerator/custom-resource-associate-hosted-zones-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-hosted-zones/runtime/src/index.ts b/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts new file mode 100644 index 000000000..d222e1d25 --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/src/index.ts @@ -0,0 +1,293 @@ +import * as AWS from 'aws-sdk'; +import { GetCallerIdentityResponse } from 'aws-sdk/clients/sts'; +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 { + assumeRoleName: string; + vpcAccountId: string; + vpcName: string; + vpcId: string; + vpcRegion: string; + hostedZoneAccountId: string; + hostedZoneIds: string[]; +} + +export class STS { + private readonly client: AWS.STS; + private readonly cache: { [roleArn: string]: AWS.Credentials } = {}; + + constructor(credentials?: AWS.Credentials) { + this.client = new AWS.STS({ + credentials, + }); + } + + async getCallerIdentity(): Promise { + return throttlingBackOff(() => this.client.getCallerIdentity().promise()); + } + + async getCredentialsForRoleArn(assumeRoleArn: string, durationSeconds: number = 3600): Promise { + if (this.cache[assumeRoleArn]) { + const cachedCredentials = this.cache[assumeRoleArn]; + const currentDate = new Date(); + if (cachedCredentials.expireTime && cachedCredentials.expireTime.getTime() < currentDate.getTime()) { + return cachedCredentials; + } + } + + const response = await throttlingBackOff(() => + this.client + .assumeRole({ + RoleArn: assumeRoleArn, + RoleSessionName: 'temporary', // TODO Generate a random name + DurationSeconds: durationSeconds, + }) + .promise(), + ); + + const stsCredentials = response.Credentials!; + const credentials = new AWS.Credentials({ + accessKeyId: stsCredentials.AccessKeyId, + secretAccessKey: stsCredentials.SecretAccessKey, + sessionToken: stsCredentials.SessionToken, + }); + this.cache[assumeRoleArn] = credentials; + return credentials; + } + + async getCredentialsForAccountAndRole( + accountId: string, + assumeRole: string, + durationSeconds?: number, + ): Promise { + return this.getCredentialsForRoleArn(`arn:aws:iam::${accountId}:role/${assumeRole}`, durationSeconds); + } +} + +const sts = new STS(); + +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 { assumeRoleName, hostedZoneAccountId, hostedZoneIds, vpcAccountId, vpcId, vpcName, vpcRegion } = properties; + + const vpcAccountCredentials = await sts.getCredentialsForAccountAndRole(vpcAccountId, assumeRoleName); + const vpcRoute53 = new AWS.Route53({ + credentials: vpcAccountCredentials, + }); + + let hostedZoneAccountCredentials: AWS.Credentials; + let hostedZoneRoute53: AWS.Route53; + if (vpcAccountId !== hostedZoneAccountId) { + hostedZoneAccountCredentials = await sts.getCredentialsForAccountAndRole(hostedZoneAccountId, assumeRoleName); + hostedZoneRoute53 = new AWS.Route53({ + credentials: hostedZoneAccountCredentials, + }); + } + + for (const hostedZoneId of hostedZoneIds) { + const hostedZoneProps = { + HostedZoneId: hostedZoneId, + VPC: { + VPCId: vpcId, + VPCRegion: vpcRegion, + }, + }; + // authorize association of VPC with Hosted zones when VPC and Hosted Zones are defined in two different accounts + if (vpcAccountId !== hostedZoneAccountId) { + await throttlingBackOff(() => hostedZoneRoute53.createVPCAssociationAuthorization(hostedZoneProps).promise()); + } + + // associate VPC with Hosted zones + try { + console.log(`Associating hosted zone ${hostedZoneId} with VPC ${vpcId} ${vpcName}...`); + await throttlingBackOff(() => vpcRoute53.associateVPCWithHostedZone(hostedZoneProps).promise()); + } catch (e) { + if (e.code === 'ConflictingDomainExists') { + console.info('Domain already added; ignore this error and continue'); + } else { + console.error(`Error while associating the hosted zone "${hostedZoneId}" to VPC "${vpcName}"`); + console.error(e); + throw new Error(e); + } + } + + // delete association of VPC with Hosted zones when VPC and Hosted Zones are defined in two different accounts + if (vpcAccountId !== hostedZoneAccountId) { + await throttlingBackOff(() => hostedZoneRoute53.deleteVPCAssociationAuthorization(hostedZoneProps).promise()); + } + } + + return { + physicalResourceId: `AssociateHostedZones-${vpcName}-${vpcRegion}-${vpcAccountId}-${hostedZoneAccountId}`, + }; +} + +async function onUpdate(event: CloudFormationCustomResourceUpdateEvent) { + const properties = (event.ResourceProperties as unknown) as HandlerProperties; + const { assumeRoleName, hostedZoneAccountId, hostedZoneIds, vpcAccountId, vpcId, vpcName, vpcRegion } = properties; + + const vpcAccountCredentials = await sts.getCredentialsForAccountAndRole(vpcAccountId, assumeRoleName); + const vpcRoute53 = new AWS.Route53({ + credentials: vpcAccountCredentials, + }); + + let hostedZoneAccountCredentials: AWS.Credentials; + let hostedZoneRoute53: AWS.Route53; + if (vpcAccountId !== hostedZoneAccountId) { + hostedZoneAccountCredentials = await sts.getCredentialsForAccountAndRole(hostedZoneAccountId, assumeRoleName); + hostedZoneRoute53 = new AWS.Route53({ + credentials: hostedZoneAccountCredentials, + }); + } + + const oldProperties = (event.OldResourceProperties as unknown) as HandlerProperties; + const currentAssociations = hostedZoneIds.filter(hz => !oldProperties.hostedZoneIds.includes(hz)); + const removeAssociations = oldProperties.hostedZoneIds.filter(hz => !hostedZoneIds.includes(hz)); + for (const hostedZoneId of currentAssociations) { + const hostedZoneProps = { + HostedZoneId: hostedZoneId, + VPC: { + VPCId: vpcId, + VPCRegion: vpcRegion, + }, + }; + // authorize association of VPC with Hosted zones when VPC and Hosted Zones are defined in two different accounts + if (vpcAccountId !== hostedZoneAccountId) { + await throttlingBackOff(() => hostedZoneRoute53.createVPCAssociationAuthorization(hostedZoneProps).promise()); + } + + // associate VPC with Hosted zones + try { + console.log(`Associating hosted zone ${hostedZoneId} with VPC ${vpcId} ${vpcName}...`); + await throttlingBackOff(() => vpcRoute53.associateVPCWithHostedZone(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); + } + } + + // delete association of VPC with Hosted zones when VPC and Hosted Zones are defined in two different accounts + if (vpcAccountId !== hostedZoneAccountId) { + await throttlingBackOff(() => hostedZoneRoute53.deleteVPCAssociationAuthorization(hostedZoneProps).promise()); + } + } + + for (const hostedZoneId of removeAssociations) { + const hostedZoneProps = { + HostedZoneId: hostedZoneId, + VPC: { + VPCId: vpcId, + VPCRegion: vpcRegion, + }, + }; + // authorize association of VPC with Hosted zones when VPC and Hosted Zones are defined in two different accounts + if (vpcAccountId !== hostedZoneAccountId) { + await throttlingBackOff(() => hostedZoneRoute53.createVPCAssociationAuthorization(hostedZoneProps).promise()); + } + + // associate VPC with Hosted zones + try { + console.log(`Disassociating hosted zone ${hostedZoneId} with VPC ${vpcId} ${vpcName}...`); + await throttlingBackOff(() => vpcRoute53.disassociateVPCFromHostedZone(hostedZoneProps).promise()); + } catch (e) { + if (e.code === 'VPCAssociationNotFound') { + console.warn(`The specified VPC "${vpcId}" and hosted zone "${hostedZoneId}" are not currently associated.`); + } else { + console.error(`Error while associating the hosted zone "${hostedZoneId}" to VPC "${vpcName}"`); + console.error(e); + throw new Error(e); + } + } + + // delete association of VPC with Hosted zones when VPC and Hosted Zones are defined in two different accounts + if (vpcAccountId !== hostedZoneAccountId) { + await throttlingBackOff(() => hostedZoneRoute53.deleteVPCAssociationAuthorization(hostedZoneProps).promise()); + } + } + + return { + physicalResourceId: `AssociateHostedZones-${vpcName}-${vpcRegion}-${vpcAccountId}-${hostedZoneAccountId}`, + }; +} + +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 { 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, + }); + + let hostedZoneAccountCredentials: AWS.Credentials; + let hostedZoneRoute53: AWS.Route53; + if (vpcAccountId !== hostedZoneAccountId) { + hostedZoneAccountCredentials = await sts.getCredentialsForAccountAndRole(hostedZoneAccountId, assumeRoleName); + hostedZoneRoute53 = new AWS.Route53({ + credentials: hostedZoneAccountCredentials, + }); + } + + for (const hostedZoneId of hostedZoneIds) { + const hostedZoneProps = { + HostedZoneId: hostedZoneId, + VPC: { + VPCId: vpcId, + VPCRegion: vpcRegion, + }, + }; + // authorize association of VPC with Hosted zones when VPC and Hosted Zones are defined in two different accounts + if (vpcAccountId !== hostedZoneAccountId) { + await throttlingBackOff(() => hostedZoneRoute53.createVPCAssociationAuthorization(hostedZoneProps).promise()); + } + + // associate VPC with Hosted zones + try { + 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); + } + + // delete association of VPC with Hosted zones when VPC and Hosted Zones are defined in two different accounts + if (vpcAccountId !== hostedZoneAccountId) { + await throttlingBackOff(() => hostedZoneRoute53.deleteVPCAssociationAuthorization(hostedZoneProps).promise()); + } + } +} diff --git a/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/tsconfig.json b/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/tsconfig.json new file mode 100644 index 000000000..118a8376a --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-hosted-zones/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-hosted-zones/runtime/webpack.config.ts b/src/lib/custom-resources/cdk-associate-hosted-zones/runtime/webpack.config.ts new file mode 100644 index 000000000..425acd8ba --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-hosted-zones/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-hosted-zones/tsconfig.json b/src/lib/custom-resources/cdk-associate-hosted-zones/tsconfig.json new file mode 100644 index 000000000..4db940b9b --- /dev/null +++ b/src/lib/custom-resources/cdk-associate-hosted-zones/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-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 diff --git a/src/lib/custom-resources/cdk-r53-dns-endpoint-ips/cdk/index.ts b/src/lib/custom-resources/cdk-r53-dns-endpoint-ips/cdk/index.ts index 36107e239..45ff0e70c 100644 --- a/src/lib/custom-resources/cdk-r53-dns-endpoint-ips/cdk/index.ts +++ b/src/lib/custom-resources/cdk-r53-dns-endpoint-ips/cdk/index.ts @@ -27,7 +27,7 @@ export class R53DnsEndpointIps extends cdk.Construct { }; this.resource = new custom.AwsCustomResource(this, 'Resource', { - resourceType: 'Custom::LogResourcePolicy', + resourceType: 'Custom::GetResolverEndpointIps', onCreate: onCreateOrUpdate, onUpdate: onCreateOrUpdate, policy: custom.AwsCustomResourcePolicy.fromStatements([ diff --git a/src/lib/custom-resources/cdk-resource-cleanup/README.md b/src/lib/custom-resources/cdk-resource-cleanup/README.md new file mode 100644 index 000000000..0f839316c --- /dev/null +++ b/src/lib/custom-resources/cdk-resource-cleanup/README.md @@ -0,0 +1,13 @@ +# Custom resource to delete the following +# - Deletes s3 bucket policy in the account with the given bucket name + +This is a custom resource to delete s3 bucket policy if exists using `deleteBucketPolicy` API call. + +## Usage + + import { ResourceCleanup } from '@aws-accelerator/custom-resource-cleanup'; + + new ResourceCleanup(accountStack, `ResourceCleanup`, { + roleArn: ``, + bucketName?: ``, + }); diff --git a/src/lib/custom-resources/cdk-resource-cleanup/cdk/index.ts b/src/lib/custom-resources/cdk-resource-cleanup/cdk/index.ts new file mode 100644 index 000000000..e02f86ca2 --- /dev/null +++ b/src/lib/custom-resources/cdk-resource-cleanup/cdk/index.ts @@ -0,0 +1,60 @@ +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'; +import { HandlerProperties } from '@aws-accelerator/custom-resource-cleanup-runtime'; + +const resourceType = 'Custom::ResourceCleanup'; + +export interface ResourceCleanupProps { + roleArn: string; + bucketName?: string; + rulesDomainNames?: string[]; + phzDomainNames?: string[]; +} + +/** + * Custom resource that will create Metric Filter on LogGroup. + */ +export class ResourceCleanup extends cdk.Construct { + private readonly resource: cdk.CustomResource; + private role: iam.IRole; + + constructor(scope: cdk.Construct, id: string, props: ResourceCleanupProps) { + super(scope, id); + this.role = iam.Role.fromRoleArn(this, `${resourceType}Role`, props.roleArn); + + const handlerProperties: HandlerProperties = { + bucketName: props.bucketName, + rulesDomainNames: props.rulesDomainNames, + phzDomainNames: props.phzDomainNames, + }; + + const runtimeProps: ResourceCleanupProps = props; + this.resource = new cdk.CustomResource(this, 'Resource', { + resourceType, + serviceToken: this.lambdaFunction.functionArn, + properties: handlerProperties, + }); + } + + 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-cleanup-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-resource-cleanup/package.json b/src/lib/custom-resources/cdk-resource-cleanup/package.json new file mode 100644 index 000000000..e1c1a978c --- /dev/null +++ b/src/lib/custom-resources/cdk-resource-cleanup/package.json @@ -0,0 +1,24 @@ +{ + "name": "@aws-accelerator/custom-resource-cleanup", + "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-cleanup-runtime": "workspace:^0.0.1" + } +} \ No newline at end of file diff --git a/src/lib/custom-resources/cdk-resource-cleanup/runtime/.gitignore b/src/lib/custom-resources/cdk-resource-cleanup/runtime/.gitignore new file mode 100644 index 000000000..1521c8b76 --- /dev/null +++ b/src/lib/custom-resources/cdk-resource-cleanup/runtime/.gitignore @@ -0,0 +1 @@ +dist diff --git a/src/lib/custom-resources/cdk-resource-cleanup/runtime/package.json b/src/lib/custom-resources/cdk-resource-cleanup/runtime/package.json new file mode 100644 index 000000000..9fee8bb2b --- /dev/null +++ b/src/lib/custom-resources/cdk-resource-cleanup/runtime/package.json @@ -0,0 +1,31 @@ +{ + "name": "@aws-accelerator/custom-resource-cleanup-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-resource-cleanup/runtime/src/index.ts b/src/lib/custom-resources/cdk-resource-cleanup/runtime/src/index.ts new file mode 100644 index 000000000..416a7cbf6 --- /dev/null +++ b/src/lib/custom-resources/cdk-resource-cleanup/runtime/src/index.ts @@ -0,0 +1,161 @@ +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; +import { CloudFormationCustomResourceEvent } from 'aws-lambda'; +import { errorHandler } from '@aws-accelerator/custom-resource-runtime-cfn-response'; +import { throttlingBackOff, delay } from '@aws-accelerator/custom-resource-cfn-utils'; + +export interface HandlerProperties { + bucketName?: string; + rulesDomainNames?: string[]; + phzDomainNames?: string[]; +} + +const s3 = new AWS.S3(); +const route53 = new AWS.Route53(); +const route53Resolver = new AWS.Route53Resolver(); + +export const handler = errorHandler(onEvent); + +async function onEvent(event: CloudFormationCustomResourceEvent) { + console.log(`Deletes resources based on input properties...`); + 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; + } +} + +async function onCreateOrUpdate(event: CloudFormationCustomResourceEvent) { + const properties = (event.ResourceProperties as unknown) as HandlerProperties; + const { bucketName, rulesDomainNames, phzDomainNames } = properties; + + // If bucket name exists, deleting the attached bucket policy + if (bucketName) { + await throttlingBackOff(() => + s3 + .deleteBucketPolicy({ + Bucket: bucketName, + }) + .promise(), + ); + } + + // If resolver rules domain names exists, delete resolver rules + // w.r.t to the domain names + for (const domain of rulesDomainNames || []) { + const resolverRuleIds = await getResolverRuleIds(domain); + for (const ruleId of resolverRuleIds || []) { + let vpcIds = await getVpcIds(ruleId!); + for (const vpcId of vpcIds || []) { + try { + await throttlingBackOff(() => + route53Resolver + .disassociateResolverRule({ + ResolverRuleId: ruleId!, + VPCId: vpcId!, + }) + .promise(), + ); + } catch (error) { + console.warn(error); + } + } + + do { + vpcIds = await getVpcIds(ruleId!); + // Waiting to disassociate VPC Ids from the resolver rule + await delay(5000); + } while ((vpcIds || []).length > 0); + + // Deleting resolver rule after disassociation of VPC Ids + try { + await throttlingBackOff(() => + route53Resolver + .deleteResolverRule({ + ResolverRuleId: ruleId!, + }) + .promise(), + ); + } catch (error) { + console.warn(error); + } + } + } + + // If private hosted zones domain names exists, delete private hosted zones + // w.r.t to the domain names + for (const domain of phzDomainNames || []) { + const privateHostedZone = await throttlingBackOff(() => + route53 + .listHostedZonesByName({ + DNSName: domain, + }) + .promise(), + ); + const hostedZoneIds = privateHostedZone.HostedZones.filter(p => p.Name === domain); + + for (const zoneId of hostedZoneIds) { + try { + await throttlingBackOff(() => + route53 + .deleteHostedZone({ + Id: zoneId.Id, + }) + .promise(), + ); + } catch (error) { + console.warn(error); + } + } + } +} + +async function getVpcIds(resolverRuleId: string) { + // Get the vpc associations for the resolver + try { + const associations = await throttlingBackOff(() => + route53Resolver + .listResolverRuleAssociations({ + Filters: [ + { + Name: 'ResolverRuleId', + Values: [resolverRuleId], + }, + ], + }) + .promise(), + ); + + const vpcIds = associations.ResolverRuleAssociations?.map(a => a.VPCId); + return vpcIds; + } catch (error) { + console.warn(error); + } +} + +async function getResolverRuleIds(domain: string) { + // Get the resolver rule details for the domain + try { + const resolverRule = await throttlingBackOff(() => + route53Resolver + .listResolverRules({ + Filters: [ + { + Name: 'DomainName', + Values: [domain], + }, + ], + }) + .promise(), + ); + return resolverRule.ResolverRules?.map(r => r.Id); + } catch (error) { + console.warn(error); + } +} diff --git a/src/lib/custom-resources/cdk-resource-cleanup/runtime/tsconfig.json b/src/lib/custom-resources/cdk-resource-cleanup/runtime/tsconfig.json new file mode 100644 index 000000000..118a8376a --- /dev/null +++ b/src/lib/custom-resources/cdk-resource-cleanup/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-resource-cleanup/runtime/webpack.config.ts b/src/lib/custom-resources/cdk-resource-cleanup/runtime/webpack.config.ts new file mode 100644 index 000000000..425acd8ba --- /dev/null +++ b/src/lib/custom-resources/cdk-resource-cleanup/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);