@fiftyten/db-toolkit
Version:
Complete database toolkit: connections, migration, and operations via AWS Session Manager
931 lines โข 45.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CloudFormationManager = void 0;
const client_cloudformation_1 = require("@aws-sdk/client-cloudformation");
const client_ec2_1 = require("@aws-sdk/client-ec2");
const client_ssm_1 = require("@aws-sdk/client-ssm");
const client_rds_1 = require("@aws-sdk/client-rds");
const chalk_1 = __importDefault(require("chalk"));
const mfa_auth_1 = require("./mfa-auth");
const cloudformation_templates_1 = require("./cloudformation-templates");
class CloudFormationManager {
constructor(region = 'us-west-1') {
this.mfaAuthenticated = false;
this.region = region;
this.cfnClient = new client_cloudformation_1.CloudFormationClient({ region });
this.ec2Client = new client_ec2_1.EC2Client({ region });
this.rdsClient = new client_rds_1.RDSClient({ region });
this.ssmClient = new client_ssm_1.SSMClient({ region });
this.mfaAuth = new mfa_auth_1.MfaAuthenticator(region);
}
/**
* Handle AWS API calls with automatic MFA authentication
*/
async callWithMfaRetry(operation) {
try {
return await operation();
}
catch (error) {
// Check if this is an MFA-related error and we haven't already authenticated
if (this.mfaAuth.isMfaRequired(error) && !this.mfaAuthenticated) {
console.log(chalk_1.default.yellow('โ ๏ธ MFA authentication required for AWS access'));
// Attempt MFA authentication
const credentials = await this.mfaAuth.authenticateWithMfa();
this.mfaAuth.applyCredentials(credentials);
// Recreate clients with new credentials
const clientConfig = {
region: this.region,
credentials: {
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
sessionToken: credentials.sessionToken
}
};
this.cfnClient = new client_cloudformation_1.CloudFormationClient(clientConfig);
this.ec2Client = new client_ec2_1.EC2Client(clientConfig);
this.rdsClient = new client_rds_1.RDSClient(clientConfig);
this.ssmClient = new client_ssm_1.SSMClient(clientConfig);
// Mark as authenticated to prevent re-prompting
this.mfaAuthenticated = true;
// Retry the operation
return await operation();
}
// Re-throw if not MFA-related or already authenticated
throw error;
}
}
/**
* Discover database security groups for automatic connectivity configuration
*/
async discoverDatabaseSecurityGroups(legacyEndpoint, targetEndpoint) {
console.log(chalk_1.default.blue('๐ Discovering database security groups...'));
const result = {};
try {
// Get all RDS instances to find matches by endpoint
const rdsResponse = await this.callWithMfaRetry(async () => {
const command = new client_rds_1.DescribeDBInstancesCommand({});
return await this.rdsClient.send(command);
});
// Find legacy database by endpoint hostname
const legacyInstance = rdsResponse.DBInstances?.find((instance) => instance.Endpoint?.Address === legacyEndpoint);
if (legacyInstance?.VpcSecurityGroups) {
result.legacySecurityGroupIds = legacyInstance.VpcSecurityGroups
.map((sg) => sg.VpcSecurityGroupId)
.filter(Boolean);
console.log(chalk_1.default.green(`โ
Found legacy database security groups: ${result.legacySecurityGroupIds.join(', ')}`));
}
else {
console.log(chalk_1.default.yellow(`โ ๏ธ Could not find legacy database with endpoint: ${legacyEndpoint}`));
}
// Find target database by endpoint hostname
const targetInstance = rdsResponse.DBInstances?.find((instance) => instance.Endpoint?.Address === targetEndpoint);
if (targetInstance?.VpcSecurityGroups) {
result.targetSecurityGroupIds = targetInstance.VpcSecurityGroups
.map((sg) => sg.VpcSecurityGroupId)
.filter(Boolean);
console.log(chalk_1.default.green(`โ
Found target database security groups: ${result.targetSecurityGroupIds.join(', ')}`));
}
else {
console.log(chalk_1.default.yellow(`โ ๏ธ Could not find target database with endpoint: ${targetEndpoint}`));
}
}
catch (error) {
console.log(chalk_1.default.yellow('โ ๏ธ Could not auto-discover database security groups'));
console.log(chalk_1.default.gray(' Security group rules will need to be configured manually'));
}
return result;
}
/**
* Get VPC and subnet information for DMS deployment
*/
async getVpcInfo(environmentName) {
try {
// Try to get VPC from storage infrastructure stack first
const storageStackName = `indicator-storage-infra-${environmentName}`;
const storageStack = await this.callWithMfaRetry(async () => {
const command = new client_cloudformation_1.DescribeStacksCommand({ StackName: storageStackName });
return await this.cfnClient.send(command);
});
const stack = storageStack.Stacks?.[0];
if (stack?.Outputs) {
const vpcId = stack.Outputs.find(o => o.OutputKey === 'VpcId')?.OutputValue;
const subnetIds = stack.Outputs
.filter(o => o.OutputKey?.includes('SubnetId'))
.map(o => o.OutputValue)
.filter(Boolean);
if (vpcId && subnetIds.length > 0) {
return { vpcId, subnetIds };
}
}
}
catch (error) {
console.log(chalk_1.default.yellow('โ ๏ธ Could not get VPC info from storage stack, trying SSM Parameter Store...'));
}
// Try to get VPC ID from SSM Parameter Store
try {
const vpcIdResponse = await this.callWithMfaRetry(async () => {
const command = new client_ssm_1.GetParameterCommand({
Name: `/indicator/shared/${environmentName}/network/vpc-id`
});
return await this.ssmClient.send(command);
});
const vpcId = vpcIdResponse.Parameter?.Value;
if (vpcId) {
console.log(chalk_1.default.green(`โ
Found VPC ID in SSM Parameter Store: ${vpcId}`));
// Get subnets for this VPC
const subnets = await this.callWithMfaRetry(async () => {
const command = new client_ec2_1.DescribeSubnetsCommand({
Filters: [
{ Name: 'vpc-id', Values: [vpcId] },
{ Name: 'state', Values: ['available'] }
]
});
return await this.ec2Client.send(command);
});
const subnetIds = subnets.Subnets?.map(s => s.SubnetId).filter(Boolean) || [];
if (subnetIds.length >= 2) {
return {
vpcId,
subnetIds: subnetIds.slice(0, 3) // DMS needs at least 2 subnets, use max 3
};
}
}
}
catch (error) {
console.log(chalk_1.default.yellow('โ ๏ธ Could not get VPC info from SSM Parameter Store, using default VPC'));
}
// Fallback to default VPC
const vpcs = await this.callWithMfaRetry(async () => {
const command = new client_ec2_1.DescribeVpcsCommand({
Filters: [{ Name: 'is-default', Values: ['true'] }]
});
return await this.ec2Client.send(command);
});
const defaultVpc = vpcs.Vpcs?.[0];
if (!defaultVpc?.VpcId) {
throw new Error('No default VPC found. Please ensure you have a default VPC or specify VPC details.');
}
const subnets = await this.callWithMfaRetry(async () => {
const command = new client_ec2_1.DescribeSubnetsCommand({
Filters: [
{ Name: 'vpc-id', Values: [defaultVpc.VpcId] },
{ Name: 'default-for-az', Values: ['true'] }
]
});
return await this.ec2Client.send(command);
});
const subnetIds = subnets.Subnets?.map(s => s.SubnetId).filter(Boolean) || [];
if (subnetIds.length === 0) {
throw new Error('No suitable subnets found for DMS deployment');
}
return {
vpcId: defaultVpc.VpcId,
subnetIds: subnetIds.slice(0, 3) // DMS needs at least 2 subnets, use max 3
};
}
/**
* Deploy CloudFormation stack
*/
async deployStack(config) {
console.log(chalk_1.default.blue('๐ Deploying migration infrastructure using CloudFormation...'));
console.log(chalk_1.default.gray(' This may take 10-15 minutes to create DMS resources'));
console.log('');
try {
// Get VPC and subnet information
console.log(chalk_1.default.blue('๐ Discovering VPC and subnet configuration...'));
const vpcInfo = await this.getVpcInfo(config.parameters.environmentName);
console.log(chalk_1.default.green(`โ
Using VPC: ${vpcInfo.vpcId}`));
console.log(chalk_1.default.green(`โ
Using subnets: ${vpcInfo.subnetIds.join(', ')}`));
console.log('');
// Discover database security groups for automatic connectivity
const targetEndpoint = config.parameters.targetEndpoint || `fiftyten-indicator-db-${config.parameters.environmentName}.cxw4cwcyepf1.us-west-1.rds.amazonaws.com`;
const securityGroups = await this.discoverDatabaseSecurityGroups(config.parameters.legacyEndpoint, targetEndpoint);
console.log('');
// Update parameters with VPC info and security groups
const templateParams = {
...config.parameters,
vpcId: vpcInfo.vpcId,
subnetIds: vpcInfo.subnetIds,
legacySecurityGroupIds: securityGroups.legacySecurityGroupIds,
targetSecurityGroupIds: securityGroups.targetSecurityGroupIds
};
// Generate CloudFormation template
const template = (0, cloudformation_templates_1.generateMigrationTemplate)(templateParams);
// Check if stack exists and its status
let stackExists = false;
let stackStatus;
try {
const response = await this.callWithMfaRetry(async () => {
const command = new client_cloudformation_1.DescribeStacksCommand({ StackName: config.stackName });
return await this.cfnClient.send(command);
});
stackExists = true;
stackStatus = response.Stacks?.[0]?.StackStatus;
}
catch (error) {
// Stack doesn't exist, which is fine
}
// Handle stack states that require deletion before recreation
const deletionRequiredStates = [
'ROLLBACK_COMPLETE',
'ROLLBACK_FAILED',
'CREATE_FAILED',
'DELETE_FAILED'
];
if (stackExists && stackStatus && deletionRequiredStates.includes(stackStatus)) {
console.log(chalk_1.default.yellow(`โ ๏ธ Found stack in ${stackStatus} state - deleting it first...`));
await this.deleteStack(config.stackName);
stackExists = false;
stackStatus = undefined;
console.log(chalk_1.default.blue('๐ Now creating fresh stack...'));
console.log('');
}
// Deploy or update stack
const operation = stackExists ? 'update' : 'create';
console.log(chalk_1.default.blue(`๐ฆ ${operation === 'create' ? 'Creating' : 'Updating'} CloudFormation stack: ${config.stackName}`));
const command = stackExists
? new client_cloudformation_1.UpdateStackCommand({
StackName: config.stackName,
TemplateBody: JSON.stringify(template, null, 2),
Capabilities: ['CAPABILITY_NAMED_IAM'],
Tags: [
{ Key: 'Environment', Value: config.parameters.environmentName },
{ Key: 'ManagedBy', Value: 'CLI' },
{ Key: 'Purpose', Value: 'Database Migration' }
]
})
: new client_cloudformation_1.CreateStackCommand({
StackName: config.stackName,
TemplateBody: JSON.stringify(template, null, 2),
Capabilities: ['CAPABILITY_NAMED_IAM'],
Tags: [
{ Key: 'Environment', Value: config.parameters.environmentName },
{ Key: 'ManagedBy', Value: 'CLI' },
{ Key: 'Purpose', Value: 'Database Migration' }
]
});
await this.callWithMfaRetry(async () => {
return await this.cfnClient.send(command);
});
// Wait for deployment to complete
await this.waitForStackOperation(config.stackName, operation);
console.log('');
console.log(chalk_1.default.green('โ
Migration infrastructure deployed successfully!'));
console.log('');
// Configure security group access after successful deployment
try {
// Get the DMS security group ID from stack outputs
const outputs = await this.getStackOutputs(config.stackName);
const dmsSecurityGroupId = outputs.DMSSecurityGroupId;
if (dmsSecurityGroupId) {
await this.configureSecurityGroupAccess(dmsSecurityGroupId, securityGroups.legacySecurityGroupIds, securityGroups.targetSecurityGroupIds, config.parameters.environmentName);
}
else {
console.log(chalk_1.default.yellow('โ ๏ธ Could not find DMS security group ID in stack outputs'));
console.log(chalk_1.default.gray(' Security group rules will need to be configured manually'));
}
}
catch (error) {
console.log(chalk_1.default.yellow('โ ๏ธ Could not configure security group access automatically'));
console.log(chalk_1.default.gray(' Security group rules will need to be configured manually'));
console.log(chalk_1.default.gray(` Error: ${error instanceof Error ? error.message : String(error)}`));
}
console.log('');
console.log(chalk_1.default.blue('๐ Next steps:'));
console.log(` 1. Start migration: ${chalk_1.default.cyan(`fiftyten-db migrate start ${config.parameters.environmentName}`)}`);
console.log(` 2. Monitor progress: ${chalk_1.default.cyan(`fiftyten-db migrate status ${config.parameters.environmentName}`)}`);
console.log(` 3. Validate data: ${chalk_1.default.cyan(`fiftyten-db migrate validate ${config.parameters.environmentName}`)}`);
}
catch (error) {
console.error(chalk_1.default.red('โ CloudFormation deployment failed:'), error instanceof Error ? error.message : String(error));
throw error;
}
}
/**
* Delete CloudFormation stack
*/
async deleteStack(stackName) {
console.log(chalk_1.default.blue('๐งน Cleaning up migration infrastructure...'));
console.log('');
try {
// Check if stack exists
let stack;
try {
const response = await this.callWithMfaRetry(async () => {
const command = new client_cloudformation_1.DescribeStacksCommand({ StackName: stackName });
return await this.cfnClient.send(command);
});
stack = response.Stacks?.[0];
}
catch (error) {
console.log(chalk_1.default.yellow('โ ๏ธ Migration stack not found - may already be cleaned up'));
return;
}
if (!stack) {
console.log(chalk_1.default.yellow('โ ๏ธ Migration stack not found - may already be cleaned up'));
return;
}
console.log(chalk_1.default.blue('๐๏ธ Deleting CloudFormation stack...'));
const command = new client_cloudformation_1.DeleteStackCommand({ StackName: stackName });
await this.callWithMfaRetry(async () => {
return await this.cfnClient.send(command);
});
// Wait for deletion to complete
await this.waitForStackOperation(stackName, 'delete');
console.log('');
console.log(chalk_1.default.green('โ
Migration infrastructure cleaned up successfully!'));
console.log(chalk_1.default.gray(' All DMS resources have been removed'));
}
catch (error) {
console.error(chalk_1.default.red('โ Failed to cleanup migration:'), error instanceof Error ? error.message : String(error));
throw error;
}
}
/**
* Wait for CloudFormation stack operation to complete
*/
async waitForStackOperation(stackName, operation) {
const targetStatuses = {
create: ['CREATE_COMPLETE'],
update: ['UPDATE_COMPLETE'],
delete: ['DELETE_COMPLETE']
};
const failureStatuses = {
create: ['CREATE_FAILED', 'ROLLBACK_COMPLETE', 'ROLLBACK_FAILED'],
update: ['UPDATE_FAILED', 'UPDATE_ROLLBACK_COMPLETE', 'UPDATE_ROLLBACK_FAILED'],
delete: ['DELETE_FAILED']
};
let lastEventId;
const startTime = Date.now();
while (true) {
try {
// Get stack status
const response = await this.callWithMfaRetry(async () => {
const command = new client_cloudformation_1.DescribeStacksCommand({ StackName: stackName });
return await this.cfnClient.send(command);
});
const stack = response.Stacks?.[0];
if (!stack) {
if (operation === 'delete') {
// Stack deleted successfully
break;
}
throw new Error('Stack not found');
}
const status = stack.StackStatus;
// Show recent events
await this.showRecentEvents(stackName, lastEventId);
// Update last event ID
try {
const eventsResponse = await this.callWithMfaRetry(async () => {
const command = new client_cloudformation_1.DescribeStackEventsCommand({ StackName: stackName });
return await this.cfnClient.send(command);
});
lastEventId = eventsResponse.StackEvents?.[0]?.EventId;
}
catch (error) {
// Ignore errors getting events
}
// Check if operation completed
if (targetStatuses[operation].includes(status)) {
break;
}
// Check if operation failed
if (failureStatuses[operation].includes(status)) {
throw new Error(`Stack ${operation} failed with status: ${status}`);
}
// Show progress indicator
const elapsed = Math.floor((Date.now() - startTime) / 1000);
process.stdout.write(`\r${chalk_1.default.blue('โณ')} ${operation}ing stack... (${elapsed}s) [${status}]`);
// Wait before next check
await new Promise(resolve => setTimeout(resolve, 10000)); // 10 seconds
}
catch (error) {
if (operation === 'delete' && error instanceof Error && error.message.includes('does not exist')) {
// Stack was deleted
break;
}
throw error;
}
}
process.stdout.write('\n'); // New line after progress indicator
}
/**
* Show recent CloudFormation events
*/
async showRecentEvents(stackName, lastEventId) {
try {
const response = await this.callWithMfaRetry(async () => {
const command = new client_cloudformation_1.DescribeStackEventsCommand({ StackName: stackName });
return await this.cfnClient.send(command);
});
const events = response.StackEvents || [];
let newEvents = [];
if (lastEventId) {
const lastIndex = events.findIndex(e => e.EventId === lastEventId);
if (lastIndex > 0) {
newEvents = events.slice(0, lastIndex).reverse();
}
}
else {
// Show last 3 events on first run
newEvents = events.slice(0, 3).reverse();
}
for (const event of newEvents) {
const timestamp = event.Timestamp?.toLocaleTimeString() || '';
const resourceType = event.ResourceType || '';
const resourceStatus = event.ResourceStatus || '';
const reason = event.ResourceStatusReason || '';
let statusColor = chalk_1.default.gray;
if (resourceStatus.includes('COMPLETE')) {
statusColor = chalk_1.default.green;
}
else if (resourceStatus.includes('FAILED')) {
statusColor = chalk_1.default.red;
}
else if (resourceStatus.includes('PROGRESS')) {
statusColor = chalk_1.default.yellow;
}
console.log(` ${chalk_1.default.gray(timestamp)} ${statusColor(resourceStatus)} ${chalk_1.default.blue(resourceType)}`);
if (reason && !reason.includes('User Initiated')) {
console.log(` ${chalk_1.default.gray(reason)}`);
}
}
}
catch (error) {
// Ignore errors showing events
}
}
/**
* Configure security group ingress rules for database access with flexible fallback strategies
*/
async configureSecurityGroupAccess(dmsSecurityGroupId, legacySecurityGroupIds, targetSecurityGroupIds, environmentName) {
console.log(chalk_1.default.blue('๐ Configuring security group access for database connectivity...'));
const allSecurityGroupIds = [
...(legacySecurityGroupIds || []),
...(targetSecurityGroupIds || [])
].filter(Boolean);
if (allSecurityGroupIds.length === 0) {
console.log(chalk_1.default.yellow('โ ๏ธ No database security groups found - skipping automatic configuration'));
console.log(chalk_1.default.gray(' You may need to manually configure security group rules for DMS access'));
return;
}
// Get VPC CIDR for fallback CIDR-based rules
let vpcCidr;
try {
const dmsSecurityGroup = await this.callWithMfaRetry(async () => {
const command = new client_ec2_1.DescribeSecurityGroupsCommand({ GroupIds: [dmsSecurityGroupId] });
return await this.ec2Client.send(command);
});
if (dmsSecurityGroup.SecurityGroups?.[0]?.VpcId) {
const vpcResponse = await this.callWithMfaRetry(async () => {
const command = new client_ec2_1.DescribeVpcsCommand({ VpcIds: [dmsSecurityGroup.SecurityGroups[0].VpcId] });
return await this.ec2Client.send(command);
});
vpcCidr = vpcResponse.Vpcs?.[0]?.CidrBlock;
}
}
catch (error) {
console.log(chalk_1.default.yellow('โ ๏ธ Could not determine VPC CIDR for fallback rules'));
}
for (const sgId of allSecurityGroupIds) {
await this.configureSingleSecurityGroup(sgId, dmsSecurityGroupId, vpcCidr, environmentName);
}
console.log(chalk_1.default.green('โ
Security group configuration completed'));
}
/**
* Remove security group rules that were added during DMS deployment
*/
async cleanupSecurityGroupRules(dmsSecurityGroupId, legacySecurityGroupIds, targetSecurityGroupIds) {
console.log(chalk_1.default.blue('๐งน Removing security group rules added for DMS connectivity...'));
const allSecurityGroupIds = [
...(legacySecurityGroupIds || []),
...(targetSecurityGroupIds || [])
].filter(Boolean);
if (allSecurityGroupIds.length === 0) {
console.log(chalk_1.default.yellow('โ ๏ธ No database security groups found - skipping rule cleanup'));
return;
}
for (const sgId of allSecurityGroupIds) {
await this.removeSingleSecurityGroupRules(sgId, dmsSecurityGroupId);
}
console.log(chalk_1.default.green('โ
Security group rule cleanup completed'));
}
/**
* Remove bidirectional security group rules for a single database security group
*/
async removeSingleSecurityGroupRules(dbSecurityGroupId, dmsSecurityGroupId) {
try {
// Remove inbound rule (DMS -> Database)
try {
await this.callWithMfaRetry(async () => {
const command = new client_ec2_1.RevokeSecurityGroupIngressCommand({
GroupId: dbSecurityGroupId,
IpPermissions: [{
IpProtocol: 'tcp',
FromPort: 5432,
ToPort: 5432,
UserIdGroupPairs: [{
GroupId: dmsSecurityGroupId,
Description: 'DMS access for migration'
}]
}]
});
return await this.ec2Client.send(command);
});
console.log(chalk_1.default.green(`โ
Removed inbound DMS rule from ${dbSecurityGroupId}`));
}
catch (error) {
// Rule might not exist, which is fine
console.log(chalk_1.default.gray(` Inbound rule already removed or doesn't exist for ${dbSecurityGroupId}`));
}
// Remove outbound rule (Database -> DMS)
try {
await this.callWithMfaRetry(async () => {
const command = new client_ec2_1.RevokeSecurityGroupEgressCommand({
GroupId: dbSecurityGroupId,
IpPermissions: [{
IpProtocol: 'tcp',
FromPort: 5432,
ToPort: 5432,
UserIdGroupPairs: [{
GroupId: dmsSecurityGroupId,
Description: 'DMS access for migration'
}]
}]
});
return await this.ec2Client.send(command);
});
console.log(chalk_1.default.green(`โ
Removed outbound DMS rule from ${dbSecurityGroupId}`));
}
catch (error) {
// Rule might not exist, which is fine
console.log(chalk_1.default.gray(` Outbound rule already removed or doesn't exist for ${dbSecurityGroupId}`));
}
}
catch (error) {
console.log(chalk_1.default.yellow(`โ ๏ธ Could not remove some rules from ${dbSecurityGroupId}: ${error instanceof Error ? error.message : String(error)}`));
}
}
/**
* Configure bidirectional security group access for a single database security group with fallback strategies
*/
async configureSingleSecurityGroup(sgId, dmsSecurityGroupId, vpcCidr, environmentName) {
try {
// Get security group details
const sgResponse = await this.callWithMfaRetry(async () => {
const command = new client_ec2_1.DescribeSecurityGroupsCommand({ GroupIds: [sgId] });
return await this.ec2Client.send(command);
});
const securityGroup = sgResponse.SecurityGroups?.[0];
if (!securityGroup) {
console.log(chalk_1.default.yellow(`โ ๏ธ Security group ${sgId} not found - skipping`));
return;
}
// Check if any PostgreSQL rules already exist for DMS access (inbound and outbound)
const hasExistingInboundRule = this.checkExistingPostgreSQLInboundRule(securityGroup, dmsSecurityGroupId, vpcCidr);
const hasExistingOutboundRule = this.checkExistingPostgreSQLOutboundRule(securityGroup, dmsSecurityGroupId, vpcCidr);
// Track configuration success
let inboundConfigured = hasExistingInboundRule;
let outboundConfigured = hasExistingOutboundRule;
// Configure inbound rule (database accepts connections from DMS)
if (hasExistingInboundRule) {
console.log(chalk_1.default.green(`โ
Inbound security group rule already exists for ${sgId}`));
}
else {
console.log(chalk_1.default.blue(`๐ Configuring inbound rule for ${sgId}...`));
if (await this.trySecurityGroupInboundReference(sgId, dmsSecurityGroupId, environmentName)) {
console.log(chalk_1.default.green(`โ
Added inbound security group rule for ${sgId}`));
inboundConfigured = true;
}
else if (vpcCidr && await this.tryVpcCidrInboundRule(sgId, vpcCidr, environmentName)) {
console.log(chalk_1.default.green(`โ
Added inbound VPC CIDR rule for ${sgId}`));
inboundConfigured = true;
}
else if (await this.tryBroadCidrInboundRule(sgId, environmentName)) {
console.log(chalk_1.default.green(`โ
Added inbound broad CIDR rule for ${sgId}`));
inboundConfigured = true;
}
else {
console.log(chalk_1.default.yellow(`โ ๏ธ Could not configure inbound rule for ${sgId}`));
}
}
// Configure outbound rule (database can respond back to DMS)
if (hasExistingOutboundRule) {
console.log(chalk_1.default.green(`โ
Outbound security group rule already exists for ${sgId}`));
}
else {
console.log(chalk_1.default.blue(`๐ Configuring outbound rule for ${sgId}...`));
if (await this.trySecurityGroupOutboundReference(sgId, dmsSecurityGroupId, environmentName)) {
console.log(chalk_1.default.green(`โ
Added outbound security group rule for ${sgId}`));
outboundConfigured = true;
}
else if (vpcCidr && await this.tryVpcCidrOutboundRule(sgId, vpcCidr, environmentName)) {
console.log(chalk_1.default.green(`โ
Added outbound VPC CIDR rule for ${sgId}`));
outboundConfigured = true;
}
else if (await this.tryBroadCidrOutboundRule(sgId, environmentName)) {
console.log(chalk_1.default.green(`โ
Added outbound broad CIDR rule for ${sgId}`));
outboundConfigured = true;
}
else {
console.log(chalk_1.default.yellow(`โ ๏ธ Could not configure outbound rule for ${sgId}`));
}
}
// Only provide manual instructions if both rules failed to configure
if (!inboundConfigured || !outboundConfigured) {
this.provideManualInstructions(sgId, dmsSecurityGroupId, vpcCidr);
}
}
catch (error) {
console.log(chalk_1.default.red(`โ Failed to configure security group ${sgId}: ${error instanceof Error ? error.message : String(error)}`));
this.provideManualInstructions(sgId, dmsSecurityGroupId, vpcCidr);
}
}
/**
* Check if PostgreSQL inbound rule already exists
*/
checkExistingPostgreSQLInboundRule(securityGroup, dmsSecurityGroupId, vpcCidr) {
return securityGroup.IpPermissions?.some((rule) => rule.IpProtocol === 'tcp' &&
rule.FromPort === 5432 &&
rule.ToPort === 5432 &&
(
// Security group reference
rule.UserIdGroupPairs?.some((pair) => pair.GroupId === dmsSecurityGroupId) ||
// VPC CIDR
(vpcCidr && rule.IpRanges?.some((range) => range.CidrIp === vpcCidr)) ||
// Broad CIDR
rule.IpRanges?.some((range) => range.CidrIp === '10.0.0.0/8'))) || false;
}
/**
* Check if PostgreSQL outbound rule already exists
*/
checkExistingPostgreSQLOutboundRule(securityGroup, dmsSecurityGroupId, vpcCidr) {
return securityGroup.IpPermissionsEgress?.some((rule) => rule.IpProtocol === 'tcp' &&
rule.FromPort === 5432 &&
rule.ToPort === 5432 &&
(
// Security group reference
rule.UserIdGroupPairs?.some((pair) => pair.GroupId === dmsSecurityGroupId) ||
// VPC CIDR
(vpcCidr && rule.IpRanges?.some((range) => range.CidrIp === vpcCidr)) ||
// Broad CIDR
rule.IpRanges?.some((range) => range.CidrIp === '10.0.0.0/8') ||
// Allow all outbound (covers our case)
rule.IpRanges?.some((range) => range.CidrIp === '0.0.0.0/0'))) || false;
}
/**
* Try adding inbound security group reference rule
*/
async trySecurityGroupInboundReference(sgId, dmsSecurityGroupId, environmentName) {
try {
const command = new client_ec2_1.AuthorizeSecurityGroupIngressCommand({
GroupId: sgId,
IpPermissions: [
{
IpProtocol: 'tcp',
FromPort: 5432,
ToPort: 5432,
UserIdGroupPairs: [
{
GroupId: dmsSecurityGroupId,
Description: `Allow DMS access for database migration - ${environmentName || 'CLI'}`
}
]
}
]
});
await this.callWithMfaRetry(async () => {
return await this.ec2Client.send(command);
});
return true;
}
catch (error) {
if (error instanceof Error && error.message.includes('already exists')) {
return true;
}
console.log(chalk_1.default.yellow(` Security group reference failed: ${error instanceof Error ? error.message : String(error)}`));
return false;
}
}
/**
* Try adding inbound VPC CIDR-based rule
*/
async tryVpcCidrInboundRule(sgId, vpcCidr, environmentName) {
try {
const command = new client_ec2_1.AuthorizeSecurityGroupIngressCommand({
GroupId: sgId,
IpPermissions: [
{
IpProtocol: 'tcp',
FromPort: 5432,
ToPort: 5432,
IpRanges: [
{
CidrIp: vpcCidr,
Description: `Allow DMS access from VPC for database migration - ${environmentName || 'CLI'}`
}
]
}
]
});
await this.callWithMfaRetry(async () => {
return await this.ec2Client.send(command);
});
return true;
}
catch (error) {
if (error instanceof Error && error.message.includes('already exists')) {
return true;
}
console.log(chalk_1.default.yellow(` VPC CIDR rule failed: ${error instanceof Error ? error.message : String(error)}`));
return false;
}
}
/**
* Try adding inbound broad CIDR rule (10.0.0.0/8)
*/
async tryBroadCidrInboundRule(sgId, environmentName) {
try {
const command = new client_ec2_1.AuthorizeSecurityGroupIngressCommand({
GroupId: sgId,
IpPermissions: [
{
IpProtocol: 'tcp',
FromPort: 5432,
ToPort: 5432,
IpRanges: [
{
CidrIp: '10.0.0.0/8',
Description: `Allow DMS access from private networks for database migration - ${environmentName || 'CLI'}`
}
]
}
]
});
await this.callWithMfaRetry(async () => {
return await this.ec2Client.send(command);
});
return true;
}
catch (error) {
if (error instanceof Error && error.message.includes('already exists')) {
return true;
}
console.log(chalk_1.default.yellow(` Broad CIDR rule failed: ${error instanceof Error ? error.message : String(error)}`));
return false;
}
}
/**
* Try adding outbound security group reference rule
*/
async trySecurityGroupOutboundReference(sgId, dmsSecurityGroupId, environmentName) {
try {
const command = new client_ec2_1.AuthorizeSecurityGroupEgressCommand({
GroupId: sgId,
IpPermissions: [
{
IpProtocol: 'tcp',
FromPort: 5432,
ToPort: 5432,
UserIdGroupPairs: [
{
GroupId: dmsSecurityGroupId,
Description: `Allow outbound PostgreSQL to DMS for database migration - ${environmentName || 'CLI'}`
}
]
}
]
});
await this.callWithMfaRetry(async () => {
return await this.ec2Client.send(command);
});
return true;
}
catch (error) {
if (error instanceof Error && error.message.includes('already exists')) {
return true;
}
console.log(chalk_1.default.yellow(` Outbound security group reference failed: ${error instanceof Error ? error.message : String(error)}`));
return false;
}
}
/**
* Try adding outbound VPC CIDR-based rule
*/
async tryVpcCidrOutboundRule(sgId, vpcCidr, environmentName) {
try {
const command = new client_ec2_1.AuthorizeSecurityGroupEgressCommand({
GroupId: sgId,
IpPermissions: [
{
IpProtocol: 'tcp',
FromPort: 5432,
ToPort: 5432,
IpRanges: [
{
CidrIp: vpcCidr,
Description: `Allow outbound PostgreSQL to VPC for DMS migration - ${environmentName || 'CLI'}`
}
]
}
]
});
await this.callWithMfaRetry(async () => {
return await this.ec2Client.send(command);
});
return true;
}
catch (error) {
if (error instanceof Error && error.message.includes('already exists')) {
return true;
}
console.log(chalk_1.default.yellow(` Outbound VPC CIDR rule failed: ${error instanceof Error ? error.message : String(error)}`));
return false;
}
}
/**
* Try adding outbound broad CIDR rule (10.0.0.0/8)
*/
async tryBroadCidrOutboundRule(sgId, environmentName) {
try {
const command = new client_ec2_1.AuthorizeSecurityGroupEgressCommand({
GroupId: sgId,
IpPermissions: [
{
IpProtocol: 'tcp',
FromPort: 5432,
ToPort: 5432,
IpRanges: [
{
CidrIp: '10.0.0.0/8',
Description: `Allow outbound PostgreSQL to private networks for DMS migration - ${environmentName || 'CLI'}`
}
]
}
]
});
await this.callWithMfaRetry(async () => {
return await this.ec2Client.send(command);
});
return true;
}
catch (error) {
if (error instanceof Error && error.message.includes('already exists')) {
return true;
}
console.log(chalk_1.default.yellow(` Outbound broad CIDR rule failed: ${error instanceof Error ? error.message : String(error)}`));
return false;
}
}
/**
* Provide manual instructions for security group configuration
*/
provideManualInstructions(sgId, dmsSecurityGroupId, vpcCidr) {
console.log(chalk_1.default.yellow(`โ ๏ธ Could not automatically configure security group ${sgId}`));
console.log(chalk_1.default.gray(''));
console.log(chalk_1.default.gray(' Manual configuration required:'));
console.log(chalk_1.default.gray(' 1. Go to EC2 Console โ Security Groups'));
console.log(chalk_1.default.gray(` 2. Find security group: ${sgId}`));
console.log(chalk_1.default.gray(' 3. Edit Inbound Rules โ Add Rule'));
console.log(chalk_1.default.gray(' 4. Type: PostgreSQL, Port: 5432'));
console.log(chalk_1.default.gray(' 5. Source options (try in order):'));
console.log(chalk_1.default.gray(` a) Security Group: ${dmsSecurityGroupId}`));
if (vpcCidr) {
console.log(chalk_1.default.gray(` b) CIDR: ${vpcCidr} (VPC range)`));
}
console.log(chalk_1.default.gray(' c) CIDR: 10.0.0.0/8 (private networks)'));
console.log(chalk_1.default.gray(''));
console.log(chalk_1.default.gray(' Or use AWS CLI:'));
console.log(chalk_1.default.gray(` aws ec2 authorize-security-group-ingress \\`));
console.log(chalk_1.default.gray(` --group-id ${sgId} \\`));
console.log(chalk_1.default.gray(` --protocol tcp --port 5432 \\`));
console.log(chalk_1.default.gray(` --source-group ${dmsSecurityGroupId}`));
console.log(chalk_1.default.gray(''));
}
/**
* Get stack outputs
*/
async getStackOutputs(stackName) {
const response = await this.callWithMfaRetry(async () => {
const command = new client_cloudformation_1.DescribeStacksCommand({ StackName: stackName });
return await this.cfnClient.send(command);
});
const stack = response.Stacks?.[0];
if (!stack?.Outputs) {
return {};
}
const outputs = {};
for (const output of stack.Outputs) {
if (output.OutputKey && output.OutputValue) {
outputs[output.OutputKey] = output.OutputValue;
}
}
return outputs;
}
}
exports.CloudFormationManager = CloudFormationManager;
//# sourceMappingURL=cloudformation-manager.js.map