Skip to content

Instantly share code, notes, and snippets.

@oxc
Last active September 17, 2025 10:34
Show Gist options
  • Select an option

  • Save oxc/2a05703efe7963217e3d77cbee614e69 to your computer and use it in GitHub Desktop.

Select an option

Save oxc/2a05703efe7963217e3d77cbee614e69 to your computer and use it in GitHub Desktop.
Script to replace CloudFormation ImportValue references with their actual resolved values. This helps break hard dependencies between stacks during development.
#!/usr/bin/env ts-node
/**
* Script to replace CloudFormation ImportValue references with their actual resolved values.
* This helps break hard dependencies between stacks during development.
*
* Usage:
* npm install aws-sdk commander
* npx tsx scripts/resolve-import-values.ts myStack.template -i '*SomeConstructPattern*'
* AWS_PROFILE=my-profile npx tsx scripts/resolve-import-values.ts --stack MyStack -o myStack.template -i '*SomeConstructPattern*'
*/
import { readFileSync, writeFileSync } from "fs";
import {
CloudFormationClient,
GetTemplateCommand,
paginateListExports,
paginateListStacks,
} from "@aws-sdk/client-cloudformation";
import { Command } from "commander";
import { minimatch } from "minimatch";
interface CloudFormationTemplate {
[key: string]: unknown;
}
interface ExportMap {
[exportName: string]: string;
}
interface ReplaceResult {
updatedTemplate: unknown;
foundImports: string[];
}
class ImportResolver {
private readonly cfClient: CloudFormationClient;
constructor(region: string, profile?: string) {
this.cfClient = new CloudFormationClient({
region,
...(profile && { profile }),
});
}
async getExportedValues(): Promise<ExportMap> {
console.log("Fetching CloudFormation exports...");
const exports: ExportMap = {};
try {
const paginator = paginateListExports({ client: this.cfClient }, {});
for await (const page of paginator) {
if (page.Exports) {
for (const exportItem of page.Exports) {
if (exportItem.Name && exportItem.Value) {
exports[exportItem.Name] = exportItem.Value;
}
}
}
}
console.log(`Found ${Object.keys(exports).length} exports`);
return exports;
} catch (error) {
throw new Error(`Error fetching exports: ${error}`);
}
}
async listStackNames(): Promise<string[]> {
try {
const stacks: string[] = [];
const paginator = paginateListStacks({ client: this.cfClient }, {});
for await (const page of paginator) {
if (page.StackSummaries) {
stacks.push(
...page.StackSummaries.filter((s) => typeof s.StackName === "string" && typeof s.StackId === "string").map(
(s) => `${s.StackName} (${s.StackId})`,
),
);
}
}
return stacks;
} catch (err) {
console.error("Error listing stacks:", err);
return [];
}
}
async getTemplateFromStack(stackName: string): Promise<CloudFormationTemplate> {
console.log(`Fetching template for stack: ${stackName}`);
try {
const command = new GetTemplateCommand({ StackName: stackName });
const response = await this.cfClient.send(command);
if (!response.TemplateBody) throw new Error("No template body returned");
return JSON.parse(response.TemplateBody);
} catch (error) {
// On error, show available stack names for debugging
const stackNames = await this.listStackNames();
throw new Error(
`Error fetching template from stack: ${error}\nAvailable stacks in region: ${stackNames.length > 0 ? stackNames.join(", ") : "(none found or permission denied)"}`,
);
}
}
async loadTemplate(
templateFile?: string,
stackName?: string,
isStack: boolean = false,
): Promise<CloudFormationTemplate> {
if (isStack && stackName) {
return await this.getTemplateFromStack(stackName);
} else if (templateFile) {
try {
const templateContent = readFileSync(templateFile, "utf-8");
return JSON.parse(templateContent);
} catch (error) {
throw new Error(`Error reading template: ${error}`);
}
} else {
throw new Error("No template source provided");
}
}
replaceImportValues(
template: unknown,
exports: ExportMap,
targetImports?: string[],
path: string = "",
): ReplaceResult {
const foundImports: string[] = [];
// Helper: match import name against patterns
const matchesPattern = (importName: string) => {
if (!targetImports || targetImports.length === 0) return true;
return targetImports.some((pattern) => minimatch(importName, pattern));
};
if (typeof template === "object" && template !== null) {
if (Array.isArray(template)) {
const updatedArray: unknown[] = [];
template.forEach((item, index) => {
const result = this.replaceImportValues(item, exports, targetImports, `${path}[${index}]`);
updatedArray.push(result.updatedTemplate);
foundImports.push(...result.foundImports);
});
return { updatedTemplate: updatedArray, foundImports };
} else {
if ("Fn::ImportValue" in template) {
const importName = template["Fn::ImportValue"];
if (typeof importName === "object" && importName !== null && "Fn::Sub" in importName) {
console.log(`Warning: Complex import value at ${path}: ${JSON.stringify(importName)}`);
console.log("You may need to manually resolve this.");
return { updatedTemplate: template, foundImports };
} else if (typeof importName === "string") {
foundImports.push(importName);
// Check if we should replace this specific import using pattern matching
const shouldReplace = matchesPattern(importName);
if (shouldReplace && importName in exports) {
console.log(`Replacing import '${importName}' with value '${exports[importName]}'`);
return { updatedTemplate: exports[importName], foundImports };
} else if (shouldReplace && !(importName in exports)) {
console.log(`ERROR: Import '${importName}' not found in exports!`);
return { updatedTemplate: template, foundImports };
} else {
console.log(`Skipping import '${importName}' (does not match any pattern)`);
return { updatedTemplate: template, foundImports };
}
}
} else {
const result: Record<string, unknown> = {};
Object.entries(template).forEach(([key, value]) => {
const childResult = this.replaceImportValues(value, exports, targetImports, `${path}.${key}`);
result[key] = childResult.updatedTemplate;
foundImports.push(...childResult.foundImports);
});
return { updatedTemplate: result, foundImports };
}
}
}
return { updatedTemplate: template, foundImports };
}
async processTemplate(
template: CloudFormationTemplate,
outputPath: string,
dryRun: boolean = false,
targetImports?: string[],
): Promise<void> {
// Get all exports
const exports = await this.getExportedValues();
// Process template and collect imports in one pass
const result = this.replaceImportValues(template, exports, targetImports);
if (result.foundImports.length === 0) {
console.log("No ImportValue references found in template");
return;
}
console.log(`\nFound ${result.foundImports.length} ImportValue references:`);
const uniqueImports = [...new Set(result.foundImports)];
uniqueImports.forEach((importVal) => {
const isTarget =
!targetImports || targetImports.length === 0 || targetImports.some((pattern) => minimatch(importVal, pattern));
const hasExport = importVal in exports;
if (isTarget && hasExport) {
console.log(` ✓ ${importVal} -> ${exports[importVal]} (REPLACED)`);
} else if (isTarget && !hasExport) {
console.log(` ✗ ${importVal} (NOT FOUND IN EXPORTS)`);
} else {
console.log(` - ${importVal} (SKIPPED - does not match any pattern)`);
}
});
if (targetImports && targetImports.length > 0) {
const foundTargets = uniqueImports.filter((imp) => targetImports.some((pattern) => minimatch(imp, pattern)));
const missingTargets = targetImports.filter((pattern) => !uniqueImports.some((imp) => minimatch(imp, pattern)));
if (missingTargets.length > 0) {
console.error(`\nError: No imports found matching patterns: ${missingTargets.join(", ")}`);
return;
}
console.log(`\nTargeted ${foundTargets.length} out of ${targetImports.length} specified patterns`);
}
if (dryRun) {
console.log("\nDry run mode - no changes made");
return;
}
console.log("\nImport values replaced in template.");
// Write output
const outputFile = outputPath;
try {
if (!outputFile) {
throw new Error("No output file specified");
}
writeFileSync(outputFile, JSON.stringify(result.updatedTemplate, null, 2));
console.log(`Updated template written to ${outputFile}`);
} catch (error) {
throw new Error(`Error writing output: ${error}`);
}
}
}
async function main() {
const program = new Command();
program
.name("resolve-imports")
.description("Replace CloudFormation ImportValue with resolved values")
.argument("[templateFile]", "Path to CloudFormation template file (optional if --stack is used)")
.option("-o, --output <file>", "Output file (default: overwrite input)")
.option("-i, --imports <imports...>", "Specific import names to replace (space-separated)")
.option("-r, --region <region>", "AWS region")
.option("-p, --profile <profile>", "AWS profile to use")
.option("--dry-run", "Show what would be replaced without making changes", false)
.option("--stack <stackName>", "CloudFormation stack name to fetch template from")
.showHelpAfterError()
.parse();
const options = program.opts();
const templateFile = program.args[0];
try {
const resolver = new ImportResolver(options.region, options.profile);
const isStack = Boolean(options.stack);
const outputPath = options.output || templateFile;
if (!outputPath) {
program.error("No output file specified");
return;
}
program.showHelpAfterError(false);
const template = await resolver.loadTemplate(templateFile, options.stack, isStack);
await resolver.processTemplate(template, outputPath, options.dry_run, options.imports);
} catch (error) {
program.error(`${error}`);
}
}
if (require.main === module) {
main().catch((error) => {
console.error("Unhandled error:", error);
process.exit(1);
});
}
export { ImportResolver };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment