serverless-tag-resources
Version:
Datamart Solution: Tag all aws resources
606 lines (581 loc) • 21.6 kB
JavaScript
"use strict";
const aws = require("aws-sdk");
class TagResourcesServerlessPlugin {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
this.awsService = this.serverless.getProvider("aws");
this.stage = this.awsService.getStage();
this.region = this.awsService.getRegion();
this.partition = this.awsService.partition || "aws";
aws.config.update({ region: this.region });
const credentials = this.awsService.getCredentials();
this.cfnService = new aws.CloudFormation(credentials);
this.ssmService = new aws.SSM(credentials);
this.iamService = new aws.IAM(credentials);
this.rdsService = new aws.RDS(credentials);
this.pinpointService = new aws.Pinpoint(credentials);
this.apigwv2Service = new aws.ApiGatewayV2(credentials);
this.firehoseService = new aws.Firehose(credentials);
this.ec2Service = new aws.EC2(credentials);
this.unsupportedTypes = [
"AWS::Lambda::Version",
"AWS::Lambda::EventSourceMapping",
"AWS::Lambda::LayerVersion",
"AWS::Lambda::EventInvokeConfig",
"AWS::Lambda::Alias",
"AWS::Lambda::Permission",
"AWS::Lambda::EventSourceMapping",
"AWS::Lambda::LayerVersionPermission",
"AWS::Lambda::Url",
"AWS::Logs::LogStream",
"AWS::Logs::Destination",
"AWS::Logs::MetricFilter",
"AWS::Logs::QueryDefinition",
"AWS::Logs::ResourcePolicy",
"AWS::Logs::SubscriptionFilter",
"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::ApiGatewayV2::Integration",
"AWS::ApiGatewayV2::Route",
"AWS::ApiGatewayV2::ApiMapping",
"AWS::ApiGatewayV2::ApiGatewayManagedOverrides",
"AWS::ApiGatewayV2::Authorizer",
"AWS::ApiGatewayV2::Deployment",
"AWS::ApiGatewayV2::Integration",
"AWS::ApiGatewayV2::IntegrationResponse",
"AWS::ApiGatewayV2::Model",
"AWS::ApiGatewayV2::Route",
"AWS::ApiGatewayV2::RouteResponse",
"AWS::ApiGateway::RequestValidator",
"AWS::ApiGateway::GatewayResponse",
"AWS::ApiGateway::Authorizer",
"AWS::AppSync::DataSource",
"AWS::AppSync::ApiKey",
"AWS::AppSync::ApiCache",
"AWS::AppSync::DomainName",
"AWS::AppSync::DomainNameApiAssociation",
"AWS::AppSync::FunctionConfiguration",
"AWS::AppSync::GraphQLSchema",
"AWS::AppSync::Resolver",
"AWS::AutoScaling::AutoScalingGroup",
"AWS::Backup::BackupVault",
"AWS::Backup::BackupSelection",
"AWS::Backup::BackupPlan",
"AWS::CodeDeploy::Application",
"AWS::CodeDeploy::DeploymentConfig",
"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::CloudWatch::Alarm",
"AWS::CloudWatch::Dashboard",
"AWS::CloudFront::CloudFrontOriginAccessIdentity",
"AWS::CloudFront::OriginAccessControl",
"AWS::CloudFront::OriginRequestPolicy",
"AWS::CloudFront::Function",
"AWS::CloudFront::ResponseHeadersPolicy",
"AWS::ElasticBeanstalk::ApplicationVersion",
"AWS::ElasticBeanstalk::ConfigurationTemplate",
"AWS::ElasticLoadBalancingV2::Listener",
"AWS::ElasticLoadBalancingV2::ListenerRule",
"AWS::ECS::ClusterCapacityProviderAssociations",
"AWS::EC2::SecurityGroupEgress",
"AWS::ECS::PrimaryTaskSet",
"AWS::EC2::LaunchTemplate",
"AWS::Events::Rule",
"AWS::Events::EventBus",
"AWS::Events::EventBusPolicy",
"AWS::Events::Connection",
"AWS::Events::ApiDestination",
"AWS::Events::Endpoint",
"AWS::Events::Archive",
"AWS::EFS::FileSystem",
"AWS::EFS::MountTarget",
"AWS::EFS::AccessPoint",
"AWS::GlobalAccelerator::Listener",
"AWS::GlobalAccelerator::EndpointGroup",
"AWS::Glue::Database",
"AWS::Glue::Crawler",
"AWS::Glue::Classifier",
"AWS::Glue::Connection",
"AWS::Glue::DataCatalogEncryptionSettings",
"AWS::Glue::Partition",
"AWS::Glue::SchemaVersion",
"AWS::Glue::SchemaVersionMetadata",
"AWS::Glue::SecurityConfiguration",
"AWS::Glue::Table",
"AWS::KMS::Alias",
"AWS::Route53::HostedZone",
"AWS::Route53::RecordSet",
"AWS::Route53::RecordSetGroup",
"AWS::Route53::HealthCheck",
"AWS::RDS::DBProxyTargetGroup",
"AWS::S3::AccessPoint",
"AWS::S3::MultiRegionAccessPoint",
"AWS::S3::MultiRegionAccessPointPolicy",
"AWS::SES::ReceiptRuleSet",
"AWS::SES::ReceiptRule",
"AWS::SES::ConfigurationSet",
"AWS::SES::ConfigurationSetEventDestination",
"AWS::SES::ReceiptFilter",
"AWS::SES::Template",
"AWS::SNS::Subscription",
"AWS::SNS::TopicPolicy",
"AWS::SQS::QueuePolicy",
"AWS::S3::BucketPolicy",
"AWS::SSM::ResourceDataSync",
"AWS::SecretsManager::SecretTargetAttachment",
"AWS::SecretsManager::RotationSchedule",
"AWS::SecretsManager::ResourcePolicy",
"AWS::IAM::Policy",
"AWS::IAM::AccessKey",
"AWS::IAM::UserToGroupAddition",
"AWS::IAM::ServiceLinkedRole",
"AWS::IAM::ManagedPolicy",
"AWS::IAM::InstanceProfile",
"AWS::IAM::Group",
"AWS::IAM::ManagedPolicy",
"AWS::IAM::RolePolicy",
"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",
"AWS::ApplicationAutoScaling::ScalableTarget",
"AWS::ApplicationAutoScaling::ScalingPolicy",
"AWS::WAFv2::WebACLAssociation",
"AWS::WAFv2::LoggingConfiguration",
"AWS::OpenSearchServerless::AccessPolicy",
"AWS::OpenSearchServerless::SecurityPolicy",
"AWS::OpenSearchServerless::VpcEndpoint",
"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",
"AWS::Pipes::Pipe",
"AWS::QLDB::Ledger",
"AWS::Scheduler::Schedule",
];
this.tagDictBasedTypes = [
"AWS::SSM::Parameter",
"AWS::Pinpoint::App",
/***Estos deben caer en algun momento***/
// "AWS::Pinpoint::Campaign",
// "AWS::Pinpoint::EmailTemplate",
// "AWS::Pinpoint::PushTemplate",
// "AWS::Pinpoint::SmsTemplate",
// "AWS::Pinpoint::Segment",
/***Fin Estos***/
"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",
];
this.otherBasedTypes = [
// "AWS::IAM::Role",
"AWS::RDS::DBCluster",
"AWS::KinesisFirehose::DeliveryStream",
];
this.haveRelatedTypes = ["AWS::EC2::Instance"];
this.hooks = {
"before:package:finalize": this.tagResources.bind(this),
"after:deploy:deploy": this.updateTagsPostDeploy.bind(this),
};
}
_getRegion() {
return this.region;
}
_getStage() {
return this.stage;
}
_getPartition() {
return this.partition;
}
_getTagNames(srcArray) {
var tagNames = [];
srcArray.forEach(function (element) {
tagNames.push(element["Key"].toLowerCase());
});
return tagNames;
}
_listBasedStackTags() {
var stackTags = [];
if (typeof this.serverless.service.provider.stackTags === "object") {
var tags = this.serverless.service.provider.stackTags;
Object.keys(tags).forEach(function (key) {
stackTags.push({ Key: key, Value: tags[key] });
});
}
//Add stage
let stageTag = [{ Key: "Stage", Value: this._getStage() }];
stackTags.concat(
stageTag.filter(
(obj) =>
this._getTagNames(stackTags).indexOf(obj["Key"].toLowerCase()) === -1
)
);
return stackTags;
}
_dictBasedStackTags() {
let stackTags = new Object();
if (typeof this.serverless.service.provider.stackTags === "object") {
stackTags = this.serverless.service.provider.stackTags;
}
//Add stage
stackTags.Stage = this._getStage();
return stackTags;
}
_excludeAWSTagsFilter(tag) {
if ("Key" in tag && tag.Key.toLowerCase().includes("aws:")) {
return false;
} else {
return true;
}
}
async getEc2Resources(reservations) {
let resources = [];
for (let reservation of reservations) {
let owner = reservation.OwnerId;
for (let instance of reservation.Instances) {
//Adding Volumes
instance.BlockDeviceMappings.forEach((volume) => {
resources.push(volume.Ebs.VolumeId);
});
//Adding NI
for (let network of instance.NetworkInterfaces) {
resources.push(network.NetworkInterfaceId);
//Verify & Adding EIP
if (network.Association) {
let association = network.Association;
if (association.IpOwnerId === owner) {
let eipParams = {
PublicIps: [association.PublicIp],
};
let addressResult = await this.ec2Service
.describeAddresses(eipParams)
.promise();
addressResult.Addresses.forEach((address) => {
resources.push(address.AllocationId);
});
}
}
//Adding SG
network.Groups.forEach((group) => {
resources.push(group.GroupId);
});
}
}
}
return resources;
}
tagDictBasedResources(objResources, logicalID) {
let stackTags = Object.assign({}, this._dictBasedStackTags());
var tags = objResources.Resources[logicalID]["Properties"]["Tags"];
if (tags) {
for (var key in stackTags) {
objResources.Resources[logicalID]["Properties"]["Tags"][key] =
stackTags[key];
}
} else {
objResources.Resources[logicalID]["Properties"]["Tags"] = stackTags;
}
//Adding/Updating Resource tag
let tagResource = false;
if (objResources.Resources[logicalID]["Properties"]["Tags"]) {
for (var key in objResources.Resources[logicalID]["Properties"]["Tags"]) {
if (key === "Resource") {
objResources.Resources[logicalID]["Properties"]["Tags"]["Resource"] =
logicalID;
tagResource = true;
}
}
}
if (!tagResource) {
objResources.Resources[logicalID]["Properties"]["Tags"]["Resource"] =
logicalID;
}
}
tagListBasedResources(objResources, logicalID) {
let stackTags = [...this._listBasedStackTags()];
var tags = objResources.Resources[logicalID]["Properties"]["Tags"];
if (tags) {
objResources.Resources[logicalID]["Properties"]["Tags"] = tags.concat(
stackTags.filter(
(obj) =>
this._getTagNames(tags).indexOf(obj["Key"].toLowerCase()) === -1
)
);
} else {
objResources.Resources[logicalID]["Properties"]["Tags"] = stackTags;
}
//Adding/Updating Resource tag
let tagResource = false;
if (objResources.Resources[logicalID]["Properties"]["Tags"]) {
objResources.Resources[logicalID]["Properties"]["Tags"].forEach((tag) => {
if (tag["Key"] === "Resource") {
tag["Value"] = logicalID;
tagResource = true;
}
});
}
if (!tagResource) {
objResources.Resources[logicalID]["Properties"]["Tags"].push({
Key: "Resource",
Value: logicalID,
});
}
}
async updateTagsPostDeploy() {
this.serverless.cli.log("TAGGING: Updating tags post deploy...");
const awsService = this.serverless.getProvider("aws");
const stackName = awsService.naming.getStackName();
const cfParams = { StackName: stackName };
let cfStackResources = await this.cfnService
.describeStackResources(cfParams)
.promise();
await this.updateDictBasedTags(cfStackResources);
await this.updateOtherResourcesTags(cfStackResources);
await this.tagRelatedResources(cfStackResources);
}
async tagRelatedResources(cfStackResources) {
this.serverless.cli.log(
"TAGGING: Updating related resources on stackTags..."
);
cfStackResources.StackResources.forEach(async (resource) => {
let tagsList = [...this._listBasedStackTags()];
// tagsList.push({ "Key": 'Resource', "Value": resource.LogicalResourceId })
if (this.haveRelatedTypes.indexOf(resource.ResourceType) !== -1) {
switch (resource.ResourceType) {
case "AWS::EC2::Instance":
let paramsEc2 = {
InstanceIds: [resource.PhysicalResourceId],
};
let instancesResult = await this.ec2Service
.describeInstances(paramsEc2)
.promise();
let ec2Tags = instancesResult.Reservations[0].Instances[0].Tags;
ec2Tags = ec2Tags.filter(this._excludeAWSTagsFilter);
let resources = await this.getEc2Resources(
instancesResult.Reservations
);
let tagsParams = {
Resources: resources,
Tags: ec2Tags,
};
await this.ec2Service.createTags(tagsParams).promise();
this.serverless.cli.log(
`Related Resources Ids tagged: ${JSON.stringify(resources)}`
);
break;
}
}
});
}
async updateOtherResourcesTags(cfStackResources) {
this.serverless.cli.log(
"TAGGING: Updating others list based resources on stackTags..."
);
cfStackResources.StackResources.forEach(async (resource) => {
let tagsList = [...this._listBasedStackTags()];
tagsList.push({ Key: "Resource", Value: resource.LogicalResourceId });
if (this.otherBasedTypes.indexOf(resource.ResourceType) !== -1) {
switch (resource.ResourceType) {
case "AWS::IAM::Role":
let iamParams = {
RoleName: resource.PhysicalResourceId,
Tags: tagsList,
};
await this.iamService.tagRole(iamParams).promise();
break;
case "AWS::RDS::DBCluster":
let cfStack = resource.StackId.split(":");
let accountId = cfStack[4];
let rdsParams = {
ResourceName: `arn:${this._getPartition()}:rds:${this._getRegion()}:${accountId}:cluster:${
resource.PhysicalResourceId
}`,
Tags: tagsList,
};
await this.rdsService.addTagsToResource(rdsParams).promise();
break;
case "AWS::KinesisFirehose::DeliveryStream":
let firehoseParams = {
DeliveryStreamName: resource.PhysicalResourceId,
Tags: tagsList,
};
await this.firehoseService
.tagDeliveryStream(firehoseParams)
.promise();
break;
}
}
});
}
async updateDictBasedTags(cfStackResources) {
this.serverless.cli.log(
"TAGGING: Updating dict based resources on stackTags..."
);
cfStackResources.StackResources.forEach(async (resource) => {
let tagsList = [...this._listBasedStackTags()];
tagsList.push({ Key: "Resource", Value: resource.LogicalResourceId });
let tagsMap = Object.assign({}, this._dictBasedStackTags());
tagsMap.Resource = resource.LogicalResourceId;
if (this.tagDictBasedTypes.indexOf(resource.ResourceType) !== -1) {
switch (resource.ResourceType) {
case "AWS::SSM::Parameter":
let ssmParams = {
ResourceId: resource.PhysicalResourceId,
ResourceType: "Parameter",
Tags: tagsList,
};
await this.ssmService.addTagsToResource(ssmParams).promise();
break;
case "AWS::Pinpoint::App":
let appParams = {
ApplicationId: resource.PhysicalResourceId,
};
let ppApp = await this.pinpointService.getApp(appParams).promise();
// this.serverless.cli.log(`ppApp: ${JSON.stringify(ppApp)}`)
let pinpParams = {
ResourceArn: ppApp.ApplicationResponse.Arn,
TagsModel: {
tags: {
...tagsMap,
},
},
};
await this.pinpointService.tagResource(pinpParams).promise();
break;
case "AWS::ApiGatewayV2::Api":
let apiv2Params = {
ResourceArn: `arn:${this._getPartition()}:apigateway:${this._getRegion()}::/apis/${
resource.PhysicalResourceId
}`,
Tags: {
...tagsMap,
},
};
await this.apigwv2Service.tagResource(apiv2Params).promise();
break;
case "AWS::ApiGatewayV2::Stage":
let apiV2 = cfStackResources.StackResources.find(
(resource) => resource.ResourceType == "AWS::ApiGatewayV2::Api"
);
let stageV2Params = {
ResourceArn: `arn:${this._getPartition()}:apigateway:${this._getRegion()}::/apis/${
apiV2.PhysicalResourceId
}/stages/${resource.PhysicalResourceId}`,
Tags: {
...tagsMap,
},
};
await this.apigwv2Service.tagResource(stageV2Params).promise();
break;
}
}
});
}
tagResources() {
this.serverless.cli.log("TAGGING: Updating tags based on stackTags...");
let self = this;
const cfTemplate =
this.serverless.service.provider.compiledCloudFormationTemplate || null;
const awsResources = this.serverless.service.resources || null;
//Tag serverless resources
if (cfTemplate && cfTemplate.Resources) {
Object.keys(cfTemplate.Resources).forEach((logicalID) => {
let resourceType = cfTemplate.Resources[logicalID]["Type"];
this.serverless.cli.log(
`TAGGING: validating resource type => ${resourceType}`
);
let stackTags = [...this._listBasedStackTags()];
if (
self.unsupportedTypes.indexOf(resourceType) == -1 &&
Array.isArray(stackTags) &&
resourceType.toLowerCase().indexOf("custom::") == -1
) {
if (cfTemplate.Resources[logicalID]["Properties"]) {
if (self.tagDictBasedTypes.indexOf(resourceType) !== -1) {
// this.tagDictBasedResources(cfTemplate, logicalID)
this.serverless.cli.log(
`TAGGING: dict based resource => ${resourceType}`
);
} else if (self.otherBasedTypes.indexOf(resourceType) == -1) {
this.tagListBasedResources(cfTemplate, logicalID);
}
} else {
self.serverless.cli.log(
"Properties not available for " + resourceType
);
}
}
if (self.unsupportedTypes.indexOf(resourceType) == -1) {
cfTemplate.Resources[logicalID]["Properties"];
}
});
}
//Tag cloudformation specified resources
if (awsResources && awsResources.Resources) {
Object.keys(awsResources.Resources).forEach((logicalID) => {
let resourceType = awsResources.Resources[logicalID]["Type"];
let stackTags = [...this._listBasedStackTags()];
if (
self.unsupportedTypes.indexOf(resourceType) == -1 &&
Array.isArray(stackTags) /*&& stackTags.length > 0*/
) {
if (awsResources.Resources[logicalID]["Properties"]) {
if (self.tagDictBasedTypes.indexOf(resourceType) !== -1) {
// this.tagDictBasedResources(awsResources, logicalID)
this.serverless.cli.log(
`TAGGING: dict based resource => ${resourceType}`
);
} else if (self.otherBasedTypes.indexOf(resourceType) == -1) {
this.tagListBasedResources(awsResources, logicalID);
}
} else {
self.serverless.cli.log(
"Properties not available for " + resourceType
);
}
}
});
}
}
}
module.exports = TagResourcesServerlessPlugin;