serverless-tag-resources
Version:
Datamart: Tag all AWS resources with dual legacy + datamart:* tag support
383 lines (370 loc) • 11.3 kB
JavaScript
;
/**
* Resource classifier.
*
* Determines how each CloudFormation resource type should be tagged:
* - 'list' → Tags: [{Key, Value}] in template
* - 'dict' → Tags: {key: value} in template + post-deploy API
* - 'api-only' → Tagged only post-deploy via AWS API (not in template)
* - 'skip' → Cannot be tagged
*/
// Resources that use dict-based tag format in CloudFormation templates
const DICT_BASED_TYPES = new Set([
"AWS::SSM::Parameter",
"AWS::Pinpoint::App",
"AWS::ApiGatewayV2::Api",
"AWS::ApiGatewayV2::Stage",
"AWS::ApiGatewayV2::DomainName",
"AWS::ApiGatewayV2::VpcLink",
"AWS::Glue::Job",
"AWS::Glue::Crawler",
"AWS::Glue::DevEndpoint",
"AWS::Glue::MLTransform",
"AWS::Glue::Trigger",
"AWS::Glue::Workflow",
"AWS::Batch::JobDefinition",
"AWS::Batch::ComputeEnvironment",
"AWS::Batch::JobQueue",
"AWS::Batch::SchedulingPolicy",
]);
// Resources tagged only via API post-deploy (not in template)
const API_ONLY_TYPES = new Set([
"AWS::RDS::DBCluster",
"AWS::KinesisFirehose::DeliveryStream",
]);
// Resources whose related resources also get tagged (volumes, ENIs, etc.)
const RELATED_TYPES = new Set([
"AWS::EC2::Instance",
]);
// Common resource types known to support list-based tags [{Key, Value}].
// Types here won't generate a warning. Types NOT in any list will be
// tagged as list-based but with a warning (unclassified).
const LIST_BASED_TYPES = new Set([
// Lambda
"AWS::Lambda::Function",
// S3
"AWS::S3::Bucket",
// DynamoDB
"AWS::DynamoDB::Table",
"AWS::DynamoDB::GlobalTable",
// SNS / SQS
"AWS::SNS::Topic",
"AWS::SQS::Queue",
// API Gateway v1
"AWS::ApiGateway::RestApi",
"AWS::ApiGateway::Stage",
"AWS::ApiGateway::UsagePlan",
"AWS::ApiGateway::DomainName",
"AWS::ApiGateway::VpcLink",
"AWS::ApiGateway::ClientCertificate",
// CloudWatch
"AWS::Logs::LogGroup",
// IAM
"AWS::IAM::Role",
"AWS::IAM::User",
// EC2 / VPC
"AWS::EC2::Instance",
"AWS::EC2::SecurityGroup",
"AWS::EC2::Subnet",
"AWS::EC2::VPC",
"AWS::EC2::InternetGateway",
"AWS::EC2::NatGateway",
"AWS::EC2::RouteTable",
"AWS::EC2::NetworkInterface",
"AWS::EC2::Volume",
"AWS::EC2::EIP",
"AWS::EC2::TransitGateway",
"AWS::EC2::TransitGatewayAttachment",
// ECS
"AWS::ECS::Cluster",
"AWS::ECS::Service",
"AWS::ECS::TaskDefinition",
// ELB
"AWS::ElasticLoadBalancingV2::LoadBalancer",
"AWS::ElasticLoadBalancingV2::TargetGroup",
// RDS
"AWS::RDS::DBInstance",
"AWS::RDS::DBSubnetGroup",
"AWS::RDS::DBParameterGroup",
"AWS::RDS::DBClusterParameterGroup",
"AWS::RDS::DBProxy",
"AWS::RDS::EventSubscription",
// ElastiCache
"AWS::ElastiCache::CacheCluster",
"AWS::ElastiCache::ReplicationGroup",
"AWS::ElastiCache::SubnetGroup",
"AWS::ElastiCache::ParameterGroup",
// CloudFront
"AWS::CloudFront::Distribution",
// KMS
"AWS::KMS::Key",
// Secrets Manager
"AWS::SecretsManager::Secret",
// Step Functions
"AWS::StepFunctions::StateMachine",
"AWS::StepFunctions::Activity",
// CodeBuild / CodePipeline
"AWS::CodeBuild::Project",
"AWS::CodePipeline::Pipeline",
// Kinesis
"AWS::Kinesis::Stream",
// OpenSearch
"AWS::OpenSearchService::Domain",
"AWS::Elasticsearch::Domain",
// WAF
"AWS::WAFv2::WebACL",
"AWS::WAFv2::IPSet",
"AWS::WAFv2::RegexPatternSet",
"AWS::WAFv2::RuleGroup",
// AppSync
"AWS::AppSync::GraphQLApi",
// Cognito (taggable ones)
// EventBridge
"AWS::Events::EventBus",
// Redshift
"AWS::Redshift::Cluster",
"AWS::Redshift::ClusterSubnetGroup",
"AWS::Redshift::ClusterParameterGroup",
// ECR
"AWS::ECR::Repository",
// GlobalAccelerator
"AWS::GlobalAccelerator::Accelerator",
// ACM
"AWS::CertificateManager::Certificate",
// SSM
"AWS::SSM::Document",
"AWS::SSM::MaintenanceWindow",
"AWS::SSM::PatchBaseline",
]);
// Resource types that do NOT support tagging.
// Using a Set for O(1) lookups. Kept as skip-list because CF adds new
// taggable types regularly and we want them tagged by default.
const SKIP_TYPES = new Set([
// Lambda
"AWS::Lambda::Version",
"AWS::Lambda::EventSourceMapping",
"AWS::Lambda::LayerVersion",
"AWS::Lambda::EventInvokeConfig",
"AWS::Lambda::Alias",
"AWS::Lambda::Permission",
"AWS::Lambda::LayerVersionPermission",
"AWS::Lambda::Url",
// CloudWatch Logs
"AWS::Logs::LogStream",
"AWS::Logs::Destination",
"AWS::Logs::MetricFilter",
"AWS::Logs::QueryDefinition",
"AWS::Logs::ResourcePolicy",
"AWS::Logs::SubscriptionFilter",
// API Gateway v1
"AWS::ApiGateway::Account",
"AWS::ApiGateway::ApiKey",
"AWS::ApiGateway::Method",
"AWS::ApiGateway::Deployment",
"AWS::ApiGateway::UsagePlanKey",
"AWS::ApiGateway::BasePathMapping",
"AWS::ApiGateway::Resource",
"AWS::ApiGateway::Model",
"AWS::ApiGateway::RequestValidator",
"AWS::ApiGateway::GatewayResponse",
"AWS::ApiGateway::Authorizer",
// API Gateway v2
"AWS::ApiGatewayV2::Integration",
"AWS::ApiGatewayV2::Route",
"AWS::ApiGatewayV2::ApiMapping",
"AWS::ApiGatewayV2::ApiGatewayManagedOverrides",
"AWS::ApiGatewayV2::Authorizer",
"AWS::ApiGatewayV2::Deployment",
"AWS::ApiGatewayV2::IntegrationResponse",
"AWS::ApiGatewayV2::Model",
"AWS::ApiGatewayV2::RouteResponse",
// AppSync
"AWS::AppSync::DataSource",
"AWS::AppSync::ApiKey",
"AWS::AppSync::ApiCache",
"AWS::AppSync::DomainName",
"AWS::AppSync::DomainNameApiAssociation",
"AWS::AppSync::FunctionConfiguration",
"AWS::AppSync::GraphQLSchema",
"AWS::AppSync::Resolver",
// AutoScaling
"AWS::AutoScaling::AutoScalingGroup",
// Backup
"AWS::Backup::BackupVault",
"AWS::Backup::BackupSelection",
"AWS::Backup::BackupPlan",
// CodeDeploy
"AWS::CodeDeploy::Application",
"AWS::CodeDeploy::DeploymentConfig",
// Cognito
"AWS::Cognito::IdentityPool",
"AWS::Cognito::IdentityPoolRoleAttachment",
"AWS::Cognito::UserPool",
"AWS::Cognito::UserPoolDomain",
"AWS::Cognito::UserPoolClient",
"AWS::Cognito::UserPoolGroup",
"AWS::Cognito::UserPoolUser",
"AWS::Cognito::UserPoolUserToGroupAttachment",
"AWS::Cognito::UserPoolIdentityProvider",
"AWS::Cognito::UserPoolResourceServer",
// CloudWatch
"AWS::CloudWatch::Alarm",
"AWS::CloudWatch::Dashboard",
// CloudFront
"AWS::CloudFront::CloudFrontOriginAccessIdentity",
"AWS::CloudFront::OriginAccessControl",
"AWS::CloudFront::OriginRequestPolicy",
"AWS::CloudFront::Function",
"AWS::CloudFront::ResponseHeadersPolicy",
"AWS::CloudFront::CachePolicy",
// Elastic Beanstalk
"AWS::ElasticBeanstalk::ApplicationVersion",
"AWS::ElasticBeanstalk::ConfigurationTemplate",
// ELB
"AWS::ElasticLoadBalancingV2::Listener",
"AWS::ElasticLoadBalancingV2::ListenerRule",
// ECS
"AWS::ECS::ClusterCapacityProviderAssociations",
"AWS::ECS::PrimaryTaskSet",
// EC2
"AWS::EC2::SecurityGroupEgress",
"AWS::EC2::SecurityGroupIngress",
"AWS::EC2::LaunchTemplate",
"AWS::EC2::VPCGatewayAttachment",
"AWS::EC2::Route",
"AWS::EC2::SubnetRouteTableAssociation",
"AWS::EC2::VPCDHCPOptionsAssociation",
"AWS::EC2::VPCEndpoint",
"AWS::EC2::TransitGatewayRoute",
"AWS::EC2::TransitGatewayRouteTableAssociation",
"AWS::EC2::TransitGatewayRouteTablePropagation",
"AWS::EC2::IPAMAllocation",
"AWS::EC2::IPAMPoolCidr",
// Events
"AWS::Events::Rule",
"AWS::Events::EventBus",
"AWS::Events::EventBusPolicy",
"AWS::Events::Connection",
"AWS::Events::ApiDestination",
"AWS::Events::Endpoint",
"AWS::Events::Archive",
// EFS
"AWS::EFS::FileSystem",
"AWS::EFS::MountTarget",
"AWS::EFS::AccessPoint",
// GlobalAccelerator
"AWS::GlobalAccelerator::Listener",
"AWS::GlobalAccelerator::EndpointGroup",
// Glue
"AWS::Glue::Database",
"AWS::Glue::Classifier",
"AWS::Glue::Crawler",
"AWS::Glue::Connection",
"AWS::Glue::DataCatalogEncryptionSettings",
"AWS::Glue::Partition",
"AWS::Glue::SchemaVersion",
"AWS::Glue::SchemaVersionMetadata",
"AWS::Glue::SecurityConfiguration",
"AWS::Glue::Table",
// Grafana
"AWS::Grafana::Workspace",
// KMS
"AWS::KMS::Alias",
// Route53
"AWS::Route53::HostedZone",
"AWS::Route53::RecordSet",
"AWS::Route53::RecordSetGroup",
"AWS::Route53::HealthCheck",
// RDS
"AWS::RDS::DBProxyTargetGroup",
// S3
"AWS::S3::AccessPoint",
"AWS::S3::MultiRegionAccessPoint",
"AWS::S3::MultiRegionAccessPointPolicy",
"AWS::S3::BucketPolicy",
// SES
"AWS::SES::ReceiptRuleSet",
"AWS::SES::ReceiptRule",
"AWS::SES::ConfigurationSet",
"AWS::SES::ConfigurationSetEventDestination",
"AWS::SES::ReceiptFilter",
"AWS::SES::Template",
// SNS / SQS
"AWS::SNS::Subscription",
"AWS::SNS::TopicPolicy",
"AWS::SQS::QueuePolicy",
// SSM
"AWS::SSM::ResourceDataSync",
// Secrets Manager
"AWS::SecretsManager::SecretTargetAttachment",
"AWS::SecretsManager::RotationSchedule",
"AWS::SecretsManager::ResourcePolicy",
// IAM
"AWS::IAM::Policy",
"AWS::IAM::AccessKey",
"AWS::IAM::UserToGroupAddition",
"AWS::IAM::ServiceLinkedRole",
"AWS::IAM::ManagedPolicy",
"AWS::IAM::InstanceProfile",
"AWS::IAM::Group",
"AWS::IAM::RolePolicy",
// Application AutoScaling
"AWS::ApplicationAutoScaling::ScalableTarget",
"AWS::ApplicationAutoScaling::ScalingPolicy",
// WAF
"AWS::WAFv2::WebACLAssociation",
"AWS::WAFv2::LoggingConfiguration",
// OpenSearch Serverless
"AWS::OpenSearchServerless::AccessPolicy",
"AWS::OpenSearchServerless::SecurityPolicy",
"AWS::OpenSearchServerless::VpcEndpoint",
// Pinpoint
"AWS::PinpointEmail::ConfigurationSetEventDestination",
"AWS::Pinpoint::ADMChannel",
"AWS::Pinpoint::APNSChannel",
"AWS::Pinpoint::APNSSandboxChannel",
"AWS::Pinpoint::APNSVoipChannel",
"AWS::Pinpoint::APNSVoipSandboxChannel",
"AWS::Pinpoint::ApplicationSettings",
"AWS::Pinpoint::BaiduChannel",
"AWS::Pinpoint::EmailChannel",
"AWS::Pinpoint::EventStream",
"AWS::Pinpoint::GCMChannel",
"AWS::Pinpoint::SMSChannel",
"AWS::Pinpoint::VoiceChannel",
// Others
"AWS::Pipes::Pipe",
"AWS::QLDB::Ledger",
"AWS::Scheduler::Schedule",
"AWS::Athena::NamedQuery",
]);
/**
* Classify a CloudFormation resource type.
*
* @param {string} resourceType - e.g. "AWS::Lambda::Function"
* @returns {'list'|'dict'|'api-only'|'related'|'skip'}
*/
function classifyResource(resourceType) {
if (!resourceType) return "skip";
// Custom resources are never tagged
if (resourceType.toLowerCase().startsWith("custom::")) return "skip";
// Check specific classifications first
if (DICT_BASED_TYPES.has(resourceType)) return "dict";
if (API_ONLY_TYPES.has(resourceType)) return "api-only";
if (RELATED_TYPES.has(resourceType)) return "related";
if (SKIP_TYPES.has(resourceType)) return "skip";
if (LIST_BASED_TYPES.has(resourceType)) return "list";
// Unknown type: attempt list-based tagging with warning
return "unclassified";
}
/**
* Check if a resource type needs post-deploy API tagging.
*/
function needsPostDeployTagging(resourceType) {
return (
DICT_BASED_TYPES.has(resourceType) ||
API_ONLY_TYPES.has(resourceType) ||
RELATED_TYPES.has(resourceType)
);
}
module.exports = { classifyResource, needsPostDeployTagging, LIST_BASED_TYPES, DICT_BASED_TYPES, API_ONLY_TYPES, RELATED_TYPES, SKIP_TYPES };