Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

aws - ec2 resize from cost hub recommendation #9281

Merged
merged 8 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 4 additions & 3 deletions c7n/filters/costhub.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,11 @@ class CostHubRecommendation(Filter):
}

permissions = ('cost-optimization-hub:ListRecommendations',)
annotation = "c7n:cost_optimize"
annotation_key = "c7n:cost_optimize"

def process(self, resources, event=None):
client = local_session(self.manager.session_factory).client('cost-optimization-hub')
client = local_session(self.manager.session_factory).client(
'cost-optimization-hub', region_name='us-east-1')
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the endpoint for hub is only in us-east-1, that endpoint does hold data for other regions

id_field = self.manager.resource_type.id
filter_params = filter_empty({
'actionTypes': [
Expand All @@ -99,7 +100,7 @@ def process(self, resources, event=None):
if not frm.filter_resources([rec], event):
continue
r = r_map[rec['resourceId']]
r[self.annotation] = rec
r[self.annotation_key] = rec
results.add(rec['resourceId'])
return [r for rid, r in r_map.items() if rid in results]

Expand Down
48 changes: 37 additions & 11 deletions c7n/resources/ec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
FilterRegistry, AgeFilter, ValueFilter, Filter
)
from c7n.filters.offhours import OffHour, OnHour
from c7n.filters.costhub import CostHubRecommendation
import c7n.filters.vpc as net_filters

from c7n.manager import resources
Expand Down Expand Up @@ -1319,7 +1320,7 @@ class Resize(BaseAction):
"""Change an instance's size.

An instance can only be resized when its stopped, this action
can optionally restart an instance if needed to effect the instance
can optionally stop/start an instance if needed to effect the instance
type change. Instances are always left in the run state they were
found in.

Expand All @@ -1328,6 +1329,24 @@ class Resize(BaseAction):
hvm/pv, and ebs optimization at minimum.

http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-resize.html

This action also has specific support for enacting recommendations
from the AWS Cost Optimization Hub for resizing.

:example:

.. code-block:: yaml

policies:
- name: ec2-rightsize
resource: aws.ec2
filters:
- type: cost-optimization
attrs:
- actionType: Rightsize
actions:
- resize

"""

schema = type_schema(
Expand Down Expand Up @@ -1361,27 +1380,26 @@ def process(self, resources):
self.log.exception(
"Exception stopping instances for resize:\n %s" % e)

client = utils.local_session(self.manager.session_factory).client('ec2')

for instance_set in utils.chunks(itertools.chain(
stopped_instances, running_instances), 20):
self.process_resource_set(instance_set)
self.process_resource_set(instance_set, client)

if self.data.get('restart') and running_instances:
client.start_instances(
InstanceIds=[i['InstanceId'] for i in running_instances])
return list(itertools.chain(stopped_instances, running_instances))

def process_resource_set(self, instance_set):
type_map = self.data.get('type-map')
default_type = self.data.get('default')

client = utils.local_session(
self.manager.session_factory).client('ec2')
def process_resource_set(self, instance_set, client):

for i in instance_set:
new_type = self.get_target_instance_type(i)
self.log.debug(
"resizing %s %s" % (i['InstanceId'], i['InstanceType']))
new_type = type_map.get(i['InstanceType'], default_type)
if new_type == i['InstanceType']:
"resizing %s %s -> %s" % (i['InstanceId'], i['InstanceType'], new_type)
)

if not new_type or new_type == i['InstanceType']:
continue
try:
client.modify_instance_attribute(
Expand All @@ -1392,6 +1410,14 @@ def process_resource_set(self, instance_set):
"Exception resizing instance:%s new:%s old:%s \n %s" % (
i['InstanceId'], new_type, i['InstanceType'], e))

def get_target_instance_type(self, i):
optimizer_recommend = i.get(CostHubRecommendation.annotation_key)
if optimizer_recommend and optimizer_recommend['actionType'] == 'Rightsize':
return optimizer_recommend['recommendedResourceSummary']
type_map = self.data.get('type-map', {})
default_type = self.data.get('default')
return type_map.get(i['InstanceType'], default_type)


@actions.register('stop')
class Stop(BaseAction):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"status_code": 200,
"data": {
"items": [
{
"accountId": "644160558196",
"actionType": "Rightsize",
"currencyCode": "USD",
"currentResourceSummary": "m5a.xlarge",
"currentResourceType": "Ec2Instance",
"estimatedMonthlyCost": 82.49,
"estimatedMonthlySavings": 43.07,
"estimatedSavingsPercentage": 35.0,
"implementationEffort": "Medium",
"lastRefreshTimestamp": {
"__class__": "datetime",
"year": 2024,
"month": 2,
"day": 6,
"hour": 14,
"minute": 31,
"second": 0,
"microsecond": 915000
},
"recommendationId": "NDkwMDY1ODg1ODYzX2YxNDhmM2ExLWUyNDQtNGM5ZC1iMDkxLTEzMjQwZTZjNGYxYg==",
"recommendationLookbackPeriodInDays": 14,
"recommendedResourceSummary": "r5a.large",
"recommendedResourceType": "Ec2Instance",
"region": "us-east-2",
"resourceArn": "arn:aws:ec2:us-east-2:644160558196:instance/i-000ce83ee0c70e572",
"resourceId": "i-000ce83ee0c70e572",
"restartNeeded": true,
"rollbackPossible": true,
"source": "ComputeOptimizer",
"tags": []
}
],
"ResponseMetadata": {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
{
"status_code": 200,
"data": {
"Reservations": [
{
"Groups": [],
"Instances": [
{
"AmiLaunchIndex": 0,
"ImageId": "ami-00399ec92321828f5",
"InstanceId": "i-000ce83ee0c70e572",
"InstanceType": "m5a.xlarge",
"KeyName": "kapil-sandbox-us-east-2",
"LaunchTime": {
"__class__": "datetime",
"year": 2021,
"month": 10,
"day": 13,
"hour": 14,
"minute": 59,
"second": 51,
"microsecond": 0
},
"Monitoring": {
"State": "disabled"
},
"Placement": {
"AvailabilityZone": "us-east-2a",
"GroupName": "",
"Tenancy": "default"
},
"PrivateDnsName": "ip-172-31-4-66.us-east-2.compute.internal",
"PrivateIpAddress": "172.31.4.66",
"ProductCodes": [],
"PublicDnsName": "",
"State": {
"Code": 80,
"Name": "stopped"
},
"StateTransitionReason": "User initiated (2024-02-08 17:03:16 GMT)",
"SubnetId": "subnet-04d3383552d0e5fcb",
"VpcId": "vpc-0011443044d315155",
"Architecture": "x86_64",
"BlockDeviceMappings": [
{
"DeviceName": "/dev/sda1",
"Ebs": {
"AttachTime": {
"__class__": "datetime",
"year": 2021,
"month": 10,
"day": 13,
"hour": 14,
"minute": 59,
"second": 52,
"microsecond": 0
},
"DeleteOnTermination": true,
"Status": "attached",
"VolumeId": "vol-07ce74970f20cf68b"
}
}
],
"ClientToken": "",
"EbsOptimized": true,
"EnaSupport": true,
"Hypervisor": "xen",
"IamInstanceProfile": {
"Arn": "arn:aws:iam::644160558196:instance-profile/assetdb-functional",
"Id": "AIPAXEGRT7KTUCEPDUG3Y"
},
"NetworkInterfaces": [
{
"Attachment": {
"AttachTime": {
"__class__": "datetime",
"year": 2021,
"month": 10,
"day": 13,
"hour": 14,
"minute": 59,
"second": 51,
"microsecond": 0
},
"AttachmentId": "eni-attach-0f1d706dba234a4d6",
"DeleteOnTermination": true,
"DeviceIndex": 0,
"Status": "attached",
"NetworkCardIndex": 0
},
"Description": "",
"Groups": [
{
"GroupName": "launch-wizard-1",
"GroupId": "sg-08b7e147e621bbd7f"
}
],
"Ipv6Addresses": [],
"MacAddress": "02:2f:0b:3b:64:be",
"NetworkInterfaceId": "eni-078156fca4353cd1a",
"OwnerId": "644160558196",
"PrivateDnsName": "ip-172-31-4-66.us-east-2.compute.internal",
"PrivateIpAddress": "172.31.4.66",
"PrivateIpAddresses": [
{
"Primary": true,
"PrivateDnsName": "ip-172-31-4-66.us-east-2.compute.internal",
"PrivateIpAddress": "172.31.4.66"
}
],
"SourceDestCheck": true,
"Status": "in-use",
"SubnetId": "subnet-04d3383552d0e5fcb",
"VpcId": "vpc-0011443044d315155",
"InterfaceType": "interface"
}
],
"RootDeviceName": "/dev/sda1",
"RootDeviceType": "ebs",
"SecurityGroups": [
{
"GroupName": "launch-wizard-1",
"GroupId": "sg-08b7e147e621bbd7f"
}
],
"SourceDestCheck": true,
"StateReason": {
"Code": "Client.UserInitiatedShutdown",
"Message": "Client.UserInitiatedShutdown: User initiated shutdown"
},
"Tags": [
{
"Key": "ActionNeeded",
"Value": "Investigate shutting down or terminating this EC2 instance which is over 365 days old"
},
{
"Key": "CostSavingsEligible",
"Value": "True"
},
{
"Key": "Owner",
"Value": "kapil@stacklet.io"
}
],
"VirtualizationType": "hvm",
"CpuOptions": {
"CoreCount": 2,
"ThreadsPerCore": 2
},
"CapacityReservationSpecification": {
"CapacityReservationPreference": "open"
},
"HibernationOptions": {
"Configured": false
},
"MetadataOptions": {
"State": "applied",
"HttpTokens": "optional",
"HttpPutResponseHopLimit": 1,
"HttpEndpoint": "enabled",
"HttpProtocolIpv6": "disabled",
"InstanceMetadataTags": "disabled"
},
"EnclaveOptions": {
"Enabled": false
},
"PlatformDetails": "Linux/UNIX",
"UsageOperation": "RunInstances",
"UsageOperationUpdateTime": {
"__class__": "datetime",
"year": 2021,
"month": 10,
"day": 13,
"hour": 14,
"minute": 59,
"second": 51,
"microsecond": 0
},
"PrivateDnsNameOptions": {
"HostnameType": "ip-name",
"EnableResourceNameDnsARecord": false,
"EnableResourceNameDnsAAAARecord": false
},
"MaintenanceOptions": {
"AutoRecovery": "default"
}
}
],
"OwnerId": "644160558196",
"ReservationId": "r-06e021d3accdc77a2"
}
],
"ResponseMetadata": {}
}
}