serverless-tag-resources
Version:
Datamart: Tag all AWS resources with dual legacy + datamart:* tag support
130 lines (114 loc) • 4.39 kB
JavaScript
;
const {
ResourceGroupsTaggingAPIClient,
UntagResourcesCommand,
} = require("@aws-sdk/client-resource-groups-tagging-api");
const {
CloudFormationClient,
DescribeStacksCommand,
DescribeStackResourcesCommand,
} = require("@aws-sdk/client-cloudformation");
const { getClient } = require("../aws-clients");
const { SKIP_TYPES } = require("../resource-classifier");
/**
* Remove unwanted tags from the stack itself and all its resources.
* Uses Resource Groups Tagging API (UntagResources) which works on
* both CF stacks and individual resources.
*
* No UpdateStack — only tag removal, zero infrastructure changes.
*/
async function removeUnwantedTags(config, stackName, tagKeysToRemove, log) {
const cfnClient = getClient(CloudFormationClient, config);
const taggingClient = getClient(ResourceGroupsTaggingAPIClient, config);
const arns = [];
// 1. Get stack ARN
const stackResult = await cfnClient.send(
new DescribeStacksCommand({ StackName: stackName })
);
const stackArn = stackResult.Stacks?.[0]?.StackId;
if (stackArn) arns.push(stackArn);
// 2. Get resource ARNs (deduplicated, excluding types that don't support tags)
const seen = new Set();
const resourceResult = await cfnClient.send(
new DescribeStackResourcesCommand({ StackName: stackName })
);
for (const resource of resourceResult.StackResources || []) {
if (SKIP_TYPES.has(resource.ResourceType)) continue;
const arn = resolveArn(resource);
if (arn && !seen.has(arn)) {
seen.add(arn);
arns.push(arn);
}
}
if (arns.length === 0) return;
// UntagResources accepts max 20 ARNs per call
const batchSize = 20;
let untagged = 0;
let skipped = 0;
for (let i = 0; i < arns.length; i += batchSize) {
const batch = arns.slice(i, i + batchSize);
try {
const result = await taggingClient.send(
new UntagResourcesCommand({
ResourceARNList: batch,
TagKeys: tagKeysToRemove,
})
);
const failedMap = result.FailedResourcesMap || {};
const failures = Object.keys(failedMap).length;
untagged += batch.length - failures;
skipped += failures;
} catch (err) {
skipped += batch.length;
}
}
log(`TAGGING: Removed [${tagKeysToRemove.join(", ")}] from ${untagged} resources (${skipped} skipped)`);
}
/**
* Resolve the ARN of a stack resource.
* PhysicalResourceId is sometimes the ARN, sometimes just the name/ID.
*/
function resolveArn(resource) {
const physicalId = resource.PhysicalResourceId;
if (!physicalId) return null;
// Already an ARN
if (physicalId.startsWith("arn:")) return physicalId;
// Extract account from stack ARN
const stackId = resource.StackId || "";
const parts = stackId.split(":");
if (parts.length < 5) return null;
const partition = parts[1];
const region = parts[3];
const account = parts[4];
// Build ARN by resource type
const type = resource.ResourceType;
const builders = {
"AWS::Lambda::Function": () =>
`arn:${partition}:lambda:${region}:${account}:function:${physicalId}`,
"AWS::SNS::Topic": () =>
`arn:${partition}:sns:${region}:${account}:${physicalId}`,
"AWS::Events::EventBus": () =>
`arn:${partition}:events:${region}:${account}:event-bus/${physicalId}`,
"AWS::Events::Rule": () =>
`arn:${partition}:events:${region}:${account}:rule/${physicalId}`,
"AWS::Logs::LogGroup": () =>
`arn:${partition}:logs:${region}:${account}:log-group:${physicalId}`,
"AWS::IAM::Role": () =>
`arn:${partition}:iam::${account}:role/${physicalId}`,
"AWS::S3::Bucket": () =>
`arn:${partition}:s3:::${physicalId}`,
"AWS::SSM::Parameter": () =>
`arn:${partition}:ssm:${region}:${account}:parameter${physicalId.startsWith("/") ? "" : "/"}${physicalId}`,
"AWS::KMS::Key": () =>
`arn:${partition}:kms:${region}:${account}:key/${physicalId}`,
"AWS::CodeBuild::Project": () =>
`arn:${partition}:codebuild:${region}:${account}:project/${physicalId}`,
"AWS::ApiGateway::RestApi": () =>
`arn:${partition}:apigateway:${region}::/restapis/${physicalId}`,
"AWS::WAFv2::WebACL": () =>
`arn:${partition}:wafv2:${region}:${account}:regional/webacl/${physicalId}`,
};
const builder = builders[type];
return builder ? builder() : null;
}
module.exports = { removeUnwantedTags };