├── .github ├── solutionid_validator.sh └── workflows │ └── maintainer_workflows.yml ├── .gitignore ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app.py ├── cdk.json ├── docs ├── SpotPlacementScore.drawio ├── SpotPlacementScore.drawio.png ├── building-a-spot-placement-score-tracker-dashboard-on-aws.png └── spot-placement-score.png ├── requirements-dev.txt ├── requirements.txt ├── source.bat ├── spot_placement_score_lambda ├── __init__.py ├── requirements.txt └── spot_placement_score_lambda.py └── sps_configuration └── sps_config.yaml /.github/solutionid_validator.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #set -e 3 | 4 | echo "checking solution id $1" 5 | echo "grep -nr --exclude-dir='.github' "$1" ./.." 6 | result=$(grep -nr --exclude-dir='.github' "$1" ./..) 7 | if [ $? -eq 0 ] 8 | then 9 | echo "Solution ID $1 found\n" 10 | echo "$result" 11 | exit 0 12 | else 13 | echo "Solution ID $1 not found" 14 | exit 1 15 | fi 16 | 17 | export result 18 | -------------------------------------------------------------------------------- /.github/workflows/maintainer_workflows.yml: -------------------------------------------------------------------------------- 1 | # Workflows managed by aws-solutions-library-samples maintainers 2 | name: Maintainer Workflows 3 | on: 4 | # Triggers the workflow on push or pull request events but only for the "main" branch 5 | push: 6 | branches: [ "main" ] 7 | pull_request: 8 | branches: [ "main" ] 9 | types: [opened, reopened, edited] 10 | 11 | jobs: 12 | CheckSolutionId: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Run solutionid validator 17 | run: | 18 | chmod u+x ./.github/solutionid_validator.sh 19 | ./.github/solutionid_validator.sh ${{ vars.SOLUTIONID }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | package-lock.json 3 | .pytest_cache 4 | *.egg-info 5 | 6 | .idea/ 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # Environments 14 | .env 15 | .venv 16 | env/ 17 | venv/ 18 | ENV/ 19 | env.bak/ 20 | venv.bak/ 21 | 22 | # CDK Context & Staging files 23 | .cdk.staging/ 24 | cdk.out/ 25 | .DS_Store 26 | configs/ 27 | .amazon-ospo-ruleset.json 28 | 29 | 30 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | CODEOWNERS @aws-solutions-library-samples/maintainers 2 | /.github/workflows/maintainer_workflows.yml @aws-solutions-library-samples/maintainers 3 | /.github/solutionid_validator.sh @aws-solutions-library-samples/maintainers 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EC2 Spot Placement Score Tracker 2 | Author: Carlos Manzanedo Rueda 3 | 4 | ## Introduction 5 | Amazon EC2 Spot Instances let you take advantage of unused EC2 capacity in the AWS cloud. 6 | Spot Instances are available at up to a 90% discount compared to On-Demand prices. 7 | Spot Placement Score (SPS) is a feature that helps AWS Spot customers by providing 8 | recommendations about which are the best suited AWS Region or Availability Zone 9 | to run a diversified configuration that adjusts to the customer requirements. 10 | 11 | Spot capacity fluctuates. You can't be sure that you'll always get the capacity that you need. 12 | A Spot placement score indicates how likely it is that a Spot request will succeed 13 | in a Region or Availability Zone. Spot placement score provides a score from 1 to 9 14 | of how successful your experience when using Spot instances would be on a set of regions. 15 | 16 | This project automates the capture of [Spot Placement Scores](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-placement-score.html) 17 | and stores SPS metrics in [CloudWatch](https://aws.amazon.com/cloudwatch/). Historic metrics 18 | can be then be visualized using CloudWatch Dashboards. CloudWatch can also be used to trigger 19 | Alarms and automation of events such as moving your workload to a region where capacity is available. 20 | 21 | Spot can be used to optimize the scale, cost and execution time of Workloads such as 22 | Containers (K8s, EKS, ECS, etc), Loosely coupled HPC and high throughput computing (AWS Batch, 23 | Parallel Cluster), Data & Analytics using Spark, Flink, Presto, CICD, Rendering, and in general 24 | any workload that is retryable, scalable and stateless. 25 | 26 | Spot instances can be managed through Auto Scaling Groups and EC2 Fleet, and controllers 27 | engines such as [Karpenter](https://karpenter.sh/). If the configuration of your workload 28 | follows Spot best practices, when a Spot instance receives a notification 29 | for termination, Auto Scaling Groups, EMR, Karpenter, etc, will automate the replacement of the 30 | instance from another Spot pool where there is capacity available. Even better! Allocation strategies 31 | such as [capacity-optimized](https://aws.amazon.com/blogs/aws/capacity-optimized-spot-instance-allocation-in-action-at-mobileye-and-skyscanner/) 32 | ,and [price-capacity-optimized](https://aws.amazon.com/blogs/compute/introducing-price-capacity-optimized-allocation-strategy-for-ec2-spot-instances/) 33 | select the optimal pools to reduce the frequency of interruption and cost for your workload. 34 | 35 | Spot placement Score considers takes as an input a diversified fleet. With this **Spot Placement 36 | Score Tracker** dashboards, you will be able to monitor and evaluate how to 37 | apply spot best practices and as a result optimize your workload to make the most 38 | of Spare capacity at scale. Some of the best practices you should consider are: 39 | * Increasing Instance Diversification. Adding instances from other sizes, and families. 40 | * Considering Flexibility in your workloads by selecting multiple Availability zones and 41 | if your workload allows, exploring the possibility of using multiple region 42 | * Considering running at times of the day when spare capacity is more available 43 | 44 | The following figure shows one of the Spot Placement Score dashboards 45 | 46 | ![img](/docs/spot-placement-score.png) 47 | 48 | 49 | ## Architecture Diagram 50 | 51 | The project provides Infrastructure as Code (IaaC) automation using [CDK](https://docs.aws.amazon.com/cdk/latest/guide/home.html) 52 | to deploy the infrastructure, IAM roles and policies required to run Lambda that gets executed 53 | every 5 minutes to collect the Spot Placement Scores of as many diversified configurations 54 | as needed. 55 | 56 | ![img](/docs/building-a-spot-placement-score-tracker-dashboard-on-aws.png) 57 | 58 | The image above shows architectural components deployed using AWS CDK. If you are not familiar with CDK you can use the 59 | [AWS Cloud 9 IDE to proceed with the whole setup and installation](#steps-to-consider-before-deployment-sps-dashboard-configuration). 60 | 61 | The CDK project sets up a few policies and roles to run with least privilege read access to all resources except 62 | for Cloudwach for which it needs to store metrics. 63 | 64 | The diagram shows how the workflow steps are invoked: 65 | 66 | * First, Event Bridge cron functionality starts the execution of the `spotPlacementScoresLambda` every 5 minutes. 67 | * The lambda function, uses the environment variable config to fetch the YAML document that contains the dashboard. 68 | * The lambda decomposes all the requests and starts requesting one by one the queries to SPS 69 | * The responses are then used to create and store CloudWatch Metrics into Cloudwatch. 70 | * The CDK project did also read the YAML document before storing it into S3 and did use the project to 71 | preset the CloudWatch representation of the dashboards. 72 | 73 | ## Important notes Spot Placement Score Limits imposed by AWS 74 | 75 | Spot placement Score API's imposes a set of limits that you should be aware of: 76 | - Limit on number of Instances, vCPU, Memory for each request. This limit will be 77 | equivalent to the number of instances that you are already using in your account 78 | in a regular way, so that you can evaluate your current workload on different regions or AZ. 79 | - Limit on number of configurations. Spot Placement Score limits you to a few (10) diversified 80 | configurations. If you configure too many configurations you may find that the lambda 81 | will fail and will be limited to just query a few of the configurations. This will also be 82 | checked as part of the CDK deployment process. 83 | 84 | The following log snippet shows one of this throttling limits in action: 85 | ```bash 86 | botocore.exceptions.ClientError: An error occurred (MaxConfigLimitExceeded) when 87 | calling the GetSpotPlacementScores operation: You have exceeded your maximum allowed 88 | Spot placement configurations. You can retry configurations that you used within the 89 | last 24 hours, or wait for 24 hours before specifying a new configuration. 90 | ``` 91 | 92 | ## Steps to consider before Deployment: SPS Dashboard Configuration 93 | 94 | The file [sps_configuration.yaml](sps_configuration/sps_config.yaml) provides an 95 | example configuration file that you can modify and adapt to your needs. This file will be 96 | used and deployed by the stack to the cloud and will be kept in S3 as the source configuration. 97 | The file uses a YAML format that follows a compatible schema as the one used by the Spot Placement 98 | Score call. You can find more information on the SPS API structure for 99 | python [here](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.get_spot_placement_scores) 100 | 101 | Before proceeding with the deployment of the dashboards CDK you will need to adapt the 102 | configuration file that defines the different Spot diversified configurations. 103 | 104 | To learn how to better adjust your configurations [keep reading the best practices section](#configuration-best-practices) 105 | and understand how to get actionable insights based on your configuration that will help you optimize your workload. 106 | 107 | 108 | ## Requirements 109 | * [CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) 110 | * Python =>3.8 111 | * [virtualenv](https://pypi.org/project/virtualenv/) 112 | * IAM Permissions run CDK stacks and request for Spot Placement Score 113 | * Docker 114 | 115 | ## Installation 116 | 117 | ### Deploying CDK project using Cloud9 IDE 118 | The easier way to setup and deploy your environment is using Cloud9 following 119 | this instructions. 120 | 121 | * Create a Cloud9 environment : 122 | * On the console run the following commands: 123 | 124 | 1.- Create a Cloud9 environment in your account. **Note**: If you use a pre-existing cloud9 environment you may need to 125 | upgrade your python and npm. 126 | 127 | 128 | 2.- Execute the following commands on Cloud 9 (you can just copy and paste) 129 | ``` 130 | export VERSION=1.0.4 131 | wget https://github.com/aws-solutions-library-samples/guidance-for-ec2-spot-placement-score-tracker/archive/refs/tags/v$VERSION.tar.gz -O ec2-spot-placement-score-tracker-v$VERSION.tar.gz 132 | tar xzvf ec2-spot-placement-score-tracker-v$VERSION.tar.gz 133 | cd $HOME/environment/guidance-for-ec2-spot-placement-score-tracker-$VERSION 134 | ``` 135 | 136 | #### Configuring the Cloud9 Setup before deployment 137 | 138 | At this stage, you can check on the Cloud 9 editor and edit the configuration file 139 | at **$HOME/environment/spot-placement-score-dashboard-cdk-v0.2.0/sps_configuration/sps_config.yaml** 140 | We do provide an example file with a few passwords, but we also recommend checking 141 | [the best practices below](#dashboard-setup-best-practices). Use those best practices to define 142 | the dashboards that are meaningful for your configuration. 143 | 144 | 145 | #### Deploy dependencies 146 | 147 | Once your configuration file is ready, we should install CDK and the rest of dependencies. 148 | ``` 149 | npm install -g --force aws-cdk 150 | pip install virtualenv 151 | virtualenv .env 152 | source .env/bin/activate 153 | pip install -r requirements.txt 154 | ``` 155 | 156 | #### Bootstrap 157 | Deploying AWS CDK apps into an AWS environment may require that you provision resources 158 | the AWS CDK needs to perform the deployment. These resources include an Amazon S3 159 | bucket for storing files and IAM roles. We will also use that S3 bucket to upload our dashboard configuration. 160 | Execute the following command to bootstrap your environment: 161 | 162 | ```bash 163 | cdk bootstrap 164 | ``` 165 | You can read more about [the bootstrapping process here](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html) 166 | 167 | #### Deploying the application & Dashboards 168 | 169 | ```bash 170 | cdk deploy 171 | ``` 172 | 173 | Once deployed, go to your AWS console and visit the CloudWatch Dashboard section. The Dashboards are aggregated 174 | with a period of 15 minutes. 175 | 176 | 177 | #### Cleanup 178 | 179 | Once you are done, you can destroy the cdk deployment and delete the cloud9 environment. 180 | You can also delete the configuration by deleting the stack in CloudFormation. 181 | ``` 182 | cdk destroy 183 | ``` 184 | 185 | **Note** the user you run this with, should be able to create Cloud9 186 | environments create deploy CloudFormation stacks, add extra IAM roles 187 | and have access to execute Spot Placement Score queries. 188 | 189 | 190 | ## Configuration 191 | 192 | The configuration file contains a YAML defined vector of dashboards. For example 193 | The following snippet shows how to configure two dashboards for two workloads. 194 | Each dashboard can define `DefaultWidgetHeight` and `DefaultWidgetWidth` to set 195 | the size of each individual chart. The maximum width of CloudWatch Grid is 24, so 196 | in this example below we will be creating rows of 2 charts of height 12. 197 | The `Sps` section defines a list of SPS configurations to evaluate. 198 | 199 | 200 | ```yaml 201 | - Dashboard: MySpsDashboard-for-Workload-A 202 | DefaultWidgetHeight: 12 # Default : 6 203 | DefaultWidgetWidth: 12 # Default : 6, Grid Maximum Width:24 204 | Sps: 205 | ... 206 | - Dashboard: MySpsDashboard-for-Workload-B 207 | DefaultWidgetHeight: 12 # Default : 6 208 | DefaultWidgetWidth: 12 # Default : 6, Grid Maximum Width:24 209 | Sps: 210 | ... 211 | ``` 212 | 213 | Now that we know how to create more than one dashboard, let's check at the SPS section. 214 | The `Sps` section defines an array of SPS configurations. Each individual `Sps` section 215 | has a named SPS query. The request format is the same as the serialised version of the 216 | call to SPS API's. You can use as a reference the boto documentation [here](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.get_spot_placement_scores) 217 | or the [aws-cli for spot placement score](https://docs.aws.amazon.com/cli/latest/reference/ec2/get-spot-placement-scores.html) 218 | Check the section **JSON Syntax** 219 | 220 | Below is an example of a Dashboard with a single SPS chart. The configuration shows a 221 | dashboard for workload A with 2 charts per row. The first chart has a name `Compute Xlarge` 222 | and uses the schemas defined in the link above to diversify over instances *c5.xlarge* and 223 | similar sized instances from the compute instance family. Aside from the key `ConfigurationName` 224 | the rest of the parameters follows the schemas provided in the links above to target european 225 | regions up to 2000 vCPUs. Note that below the configuration `Compute Xlarge`, there is a second 226 | one for `Compute 2Xlarge`. 227 | 228 | ```yaml 229 | - Dashboard: MySpsDashboard-for-Workload-A 230 | DefaultWidgetHeight: 12 # Default : 6 231 | DefaultWidgetWidth: 12 # Default : 6, Grid Maximum Width:24 232 | Sps: 233 | # Second configuration this one for Compute 2xlarge 234 | - ConfigurationName: Compute Xlarge 235 | InstanceTypes: 236 | - c5.xlarge 237 | - c6i.xlarge 238 | - c5a.xlarge 239 | - c5d.xlarge 240 | ... 241 | RegionNames: 242 | - eu-west-1 243 | - eu-west-2 244 | ... 245 | SingleAvailabilityZone: False 246 | TargetCapacity: 2000 247 | TargetCapacityUnitType: vcpu 248 | 249 | # Second configuration this one for Compute 2xlarge 250 | - ConfigurationName: Compute 2Xlarge 251 | ... 252 | ``` 253 | 254 | Instead of using `InstanceTypes` we do recommend using `InstanceRequirementsWithMetadata`. This 255 | maps with requesting Diversification using Instance attributes rather than the AWS instance names. 256 | You can read more about [Attribute Based Instance Selection](https://aws.amazon.com/blogs/aws/new-attribute-based-instance-type-selection-for-ec2-auto-scaling-and-ec2-fleet/) 257 | We **strongly recommend** to define your configurations using Attribute Based Instance Selection. 258 | By doing that you will have a simple configuration to maximise the diversification and instance types 259 | that your workload can use and that will consider new instances as they are released by AWS. 260 | 261 | ```yaml 262 | - Dashboard: MySpsDashboard-for-Workload-A 263 | DefaultWidgetHeight: 12 # Default : 6 264 | DefaultWidgetWidth: 12 # Default : 6, Grid Maximum Width:24 265 | Sps: 266 | # Second configuration this one for Compute 2xlarge 267 | - ConfigurationName: Compute Xlarge 268 | InstanceRequirementsWithMetadata: 269 | ArchitectureTypes: 270 | - x86_64 271 | InstanceRequirements: 272 | VCpuCount: 273 | Min: 32 274 | MemoryMiB: 275 | Min: 256 276 | AcceleratorCount: 277 | Max: 0 278 | BareMetal: excluded 279 | BurstablePerformance: excluded 280 | CpuManufacturers: 281 | - intel 282 | - amd 283 | InstanceGenerations: 284 | - current 285 | MemoryGiBPerVCpu: 286 | Min: 8 287 | SpotMaxPricePercentageOverLowestPrice: 50 288 | 289 | # Second configuration this one for Compute 2xlarge 290 | - ConfigurationName: Compute 2Xlarge 291 | ... 292 | ``` 293 | 294 | ### Advanced configurations 295 | 296 | The configuration file, by default supports the definition of multiple dashboards, 297 | but still in some scenarios you may want to have multiple configuration files, 298 | or deploy multiple times a CloudFormation stack with a different name and a different 299 | configuration. 300 | 301 | #### Creating a stack with a different configuration file 302 | 303 | The default configuration file is stored in the `sps_configuration/sps_config.yaml`. 304 | You can point to any other file by using the context key `sps-config` in when launching 305 | cdk commands: 306 | ```bash 307 | cdk deploy --context "sps-config=./my_sps_dashboard_configuration.yaml" 308 | ``` 309 | 310 | #### Creating and deploying multiple stacks on the same AWS account 311 | 312 | In some situations you may want to deploy a two different configuration files simultaneously on 313 | the same account. You can do it by using the following command 314 | ```bash 315 | cdk deploy --context "sps-config=./my_sps_dashboard_configuration.yaml" --context "stack-name=my-sps-demo" 316 | ``` 317 | 318 | This will create a new Stack named `my-sps-demo`. To destroy/remove the stack you can use CloudFormation 319 | directly. 320 | 321 | ## Dashboard Setup Best Practices 322 | 323 | Checking out what is the Spot Placement Score is definitely useful. You can use this project and 324 | [Spot Interruption Dashboard](https://github.com/aws-samples/ec2-spot-interruption-dashboard) 325 | to get understand and get the right observability for your workloads, but that's just the begining. 326 | 327 | The goal when we set up SPS dashboard is to find actionable configurations that will help to improve 328 | the way that our workload provisions Spot capacity at the scale you need. The next steps will guide you 329 | on a set of steps to define your dashboard configuration. 330 | 331 | * Consider using a dashboard per workload. We will focus our attention at the workload level and will 332 | evaluate which other configurations can improve our current configurations. 333 | 334 | * Understand your workload requirements and find: (a) how many vCPUs you will need, (b) what is the minimum 335 | configuration that qualifies for your workload (c) can the workload be spread across AZ's ? (d) Which 336 | regions can your organization use, and which ones are you considering using in the future. Set the first 337 | configuration of the dashboard to be your current workload configuration defined in this step. 338 | 339 | * Decide which other configurations you'd like to compare your current one against and how that will 340 | increase diversification. Select up to 3 Configurations from the ones you think have more chances to increase 341 | your access to spare capacity. 3 or 4 is enough adding more configurations can make an analysis confusing ( 342 | and you can try others later). 343 | 344 | * To consider new configuration you can use a mix of these techniques such as: 345 | (a)using Attribute Instance Selection instead of a list of instances (b) Think of using instances of 346 | larger sizes, or smaller sizes if appropriate for your workload (c) Consider expanding over all Availability 347 | zones if you have not done it yet (and is appropriate for your workload) 348 | 349 | * Consider adding potential regions where your workload could run in the future. Think capacity pools may have 350 | seasonality, which you can use to run your time flexible workload at a different time, find the next 351 | region to expand on, or find where you'd run your Disaster Recovery regional workload copy. 352 | 353 | * Create the extra configurations in the same dashboard. Make sure the properties for `RegionNames`, 354 | `SingleAvailabilityZone` and `TargetCapacity` stay the same so you can compare the configurations like for like. 355 | 356 | * Adapt the dashboard `DefaultWidgetWidth` to define how many charts/configurations you want per row. 357 | For example if you have 4 configurations, you can set the `DefaultWidgetWidth` to 6 so that each row contains 358 | the 4 configurations side by side, making them easier to compare. 359 | 360 | * With the first row already configured, we will follow the same pattern in the second row. We can make a copy of 361 | all the configurations, and then change just one dimension. The idea is that we can use the row / column patter to 362 | identify configurations. For example we could chose the `TargetCapacity` dimension, copying all the previous configuration 363 | and then checking what would happen if our workload doubles in size, or if we could perhaps reduce in two and run 364 | two copies in different regions. 365 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | ### Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | ### SPDX-License-Identifier: MIT-0 3 | ### 4 | ### Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | ### software and associated documentation files (the "Software"), to deal in the Software 6 | ### without restriction, including without limitation the rights to use, copy, modify, 7 | ### merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | ### permit persons to whom the Software is furnished to do so. 9 | ### 10 | ### THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | ### INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | ### PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | ### HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | ### OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | ### SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | # 17 | # Author: Carlos Manzanedo Rueda 18 | 19 | #!/usr/bin/env python3 20 | import os 21 | 22 | import aws_cdk 23 | import aws_cdk as cdk 24 | import aws_cdk.aws_lambda_python_alpha as lambda_python 25 | from spot_placement_score_lambda import spot_placement_score_lambda as sps 26 | from constructs import Construct 27 | from aws_cdk import ( 28 | Duration, 29 | Stack, 30 | aws_s3_assets, 31 | aws_lambda, 32 | aws_logs, 33 | aws_iam, 34 | aws_events, 35 | aws_events_targets, 36 | aws_cloudwatch, 37 | Names, 38 | CfnOutput, 39 | Aspects 40 | ) 41 | from cdk_nag import ( 42 | AwsSolutionsChecks, 43 | NagSuppressions 44 | ) 45 | 46 | SPS_LAMBDA_ASSET_S3_BUCKET = 'sps-lambda-asset-s3-bucket' 47 | CONTEXT_FILE_CONFIGURATION_KEY = 'sps-config' 48 | CONTEXT_CDK_STACK_NAME_KEY = 'cfn-name' 49 | DEFAULT_STACK_NAME = "spot-placement-score-dashboard" 50 | DEFAULT_CONFIGURATION_FILE = './sps_configuration/sps_config.yaml' 51 | COLOR_LIST = [ 52 | '#1f77b4', 53 | '#ff7f0e', 54 | '#2ca02c', 55 | '#d62728', 56 | '#9467bd', 57 | '#8c564b', 58 | '#e377c2', 59 | '#7f7f7f', 60 | '#bcbd22', 61 | '#17becf', 62 | '#aec7e8', 63 | '#ffbb78', 64 | '#98df8a', 65 | '#ff9896', 66 | '#c5b0d5', 67 | '#c49c94', 68 | '#f7b6d2', 69 | '#c7c7c7', 70 | '#dbdb8d', 71 | '#9edae5' 72 | ] 73 | 74 | 75 | class SpotPlacementScoreDashboardStack(Stack): 76 | 77 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 78 | super().__init__(scope, construct_id, **kwargs) 79 | 80 | account = aws_cdk.Environment.account 81 | region = aws_cdk.Environment.region 82 | 83 | configuration_file = self.node.try_get_context(CONTEXT_FILE_CONFIGURATION_KEY) \ 84 | if self.node.try_get_context(CONTEXT_FILE_CONFIGURATION_KEY)\ 85 | else DEFAULT_CONFIGURATION_FILE 86 | 87 | lambda_s3_configuration = aws_s3_assets.Asset( 88 | self, 89 | f"{SPS_LAMBDA_ASSET_S3_BUCKET}--{Names.unique_id(self)}", 90 | path=configuration_file 91 | ) 92 | 93 | self.tags.set_tag("aws_solution_name", "SPS-Dashboard") 94 | self.tags.set_tag("sps-dashboard", self.to_string()) 95 | 96 | sps_lambda_role = aws_iam.Role( 97 | self, 98 | f"SPS-lambda-role-{Names.unique_id(self)}", 99 | assumed_by=aws_iam.ServicePrincipal('lambda.amazonaws.com'), 100 | ) 101 | 102 | read_from_s3_bucket_policy = aws_iam.PolicyStatement( 103 | effect=aws_iam.Effect.ALLOW, 104 | resources=[f"arn:aws:s3:::{lambda_s3_configuration.s3_bucket_name}/*"], 105 | actions=[ 106 | 's3:GetObject' 107 | ] 108 | ) 109 | 110 | # Spot Placement Score support conditional iam property for regions 111 | # Customer can protect the regions that are selected by adding that 112 | # condition here 113 | read_sps_policy = aws_iam.PolicyStatement( 114 | effect=aws_iam.Effect.ALLOW, 115 | resources=['*'], 116 | actions=[ 117 | 'ec2:GetSpotPlacementScores' 118 | ] 119 | ) 120 | 121 | put_to_cloudwatch = aws_iam.PolicyStatement( 122 | effect=aws_iam.Effect.ALLOW, 123 | resources=["*"], 124 | actions=[ 125 | 'cloudwatch:PutMetricData' 126 | ], 127 | conditions={ 128 | "StringEquals": { 129 | "cloudwatch:namespace": sps.SPS_METRIC_NAMESPACE 130 | } 131 | } 132 | ) 133 | 134 | basic_lambda_policy_for_logs = aws_iam.PolicyStatement( 135 | effect=aws_iam.Effect.ALLOW, 136 | resources=["*"], 137 | actions=[ 138 | 'logs:CreateLogGroup', 139 | 'logs:CreateLogStream', 140 | 'logs:PutLogEvents' 141 | ] 142 | ) 143 | 144 | sps_lambda_role.add_to_policy(basic_lambda_policy_for_logs) 145 | sps_lambda_role.add_to_policy(read_sps_policy) 146 | sps_lambda_role.add_to_policy(read_from_s3_bucket_policy) 147 | sps_lambda_role.add_to_policy(put_to_cloudwatch) 148 | # The PutMetric data does only support protection by using the 149 | # Conditional for the namespace. I've restricted the namespace 150 | # to the only one allowed by our app 151 | # Additionally ec2:GetSpotPlacementScores is a read only API 152 | # that just does get the Spot placement score with no resource 153 | # allocated except regions... and in this case we definitely 154 | # want to have it to * as we want to query SPS across regions 155 | NagSuppressions.add_resource_suppressions(sps_lambda_role, [({ 156 | 'id': 'AwsSolutions-IAM5', 157 | 'reason': 'Role used adheres to least privilege, PutMetricData does only '+ 158 | 'support * parameter, but the code uses also a IAM conditional' + 159 | 'to restrict write operations to the ' + sps.SPS_METRIC_NAMESPACE + 160 | 'the other * is needed for the read method `ec2:GetSpotPlacementScores'}) 161 | ], apply_to_children=True) 162 | 163 | sps_lambda_log_retention_role = aws_iam.Role( 164 | self, 165 | f"SPS-lambda-log-retention-role-{Names.unique_id(self)}", 166 | assumed_by=aws_iam.ServicePrincipal('lambda.amazonaws.com'), 167 | ) 168 | 169 | log_retention_policy = aws_iam.PolicyStatement( 170 | effect=aws_iam.Effect.ALLOW, 171 | resources=["*"], 172 | actions=[ 173 | 'logs:DeleteRetentionPolicy', 174 | 'logs:PutRetentionPolicy', 175 | 'logs:CreateLogGroup', 176 | 'logs:CreateLogStream', 177 | 'logs:PutLogEvents' 178 | ] 179 | ) 180 | sps_lambda_log_retention_role.add_to_policy(log_retention_policy) 181 | NagSuppressions.add_resource_suppressions(sps_lambda_log_retention_role, [({ 182 | 'id': 'AwsSolutions-IAM5', 183 | 'reason': 'Lambda for log cleaning requires access to the log group, same for rotation'}) 184 | ], apply_to_children=True) 185 | 186 | sps_lambda = lambda_python.PythonFunction( 187 | self, 188 | f"SPS-score-function--{Names.unique_id(self)}", 189 | entry='./spot_placement_score_lambda/', 190 | index='spot_placement_score_lambda.py', 191 | handler='handler', 192 | runtime=aws_lambda.Runtime.PYTHON_3_9, 193 | log_retention=aws_logs.RetentionDays.FIVE_DAYS, # Retention logs for the lambda set to 5 days 194 | description="Spot Placement Score to Cloudwatch lambda, stores sps into cloudwatch", 195 | memory_size=256, 196 | profiling=False, # Should be disabled for prod use 197 | timeout=Duration.seconds(300), 198 | environment={ 199 | "S3_CONFIGURATION_BUCKET": lambda_s3_configuration.s3_bucket_name, 200 | "S3_CONFIGURATION_OBJECT_KEY": lambda_s3_configuration.s3_object_key 201 | }, 202 | role=sps_lambda_role, 203 | log_retention_role=sps_lambda_log_retention_role, 204 | reserved_concurrent_executions=1 205 | ) 206 | 207 | 208 | rule = aws_events.Rule( 209 | self, 210 | f"CollectSPS--{Names.unique_id(self)}", 211 | schedule=aws_events.Schedule.cron( 212 | minute='*/5', 213 | hour='*', 214 | month='*', 215 | week_day='*', 216 | year='*'), 217 | ) 218 | rule.add_target(aws_events_targets.LambdaFunction(sps_lambda)) 219 | 220 | # Setting the configuration for local execution 221 | os.environ[sps.DEBUG] = "true" 222 | os.environ[sps.DEBUG_CONFIG_FILE] = configuration_file 223 | dashboard_config_list = sps.loadConfigurations() 224 | dashboards = [] 225 | # TODO: Run python cerberus validation on the configuration 226 | for dashboard_config in dashboard_config_list: 227 | widgets = [] 228 | metric_colors = {} 229 | colour_counter = 0 230 | 231 | dashboard_name = dashboard_config['Dashboard'] 232 | sps_configurations = dashboard_config['Sps'] 233 | width = 24 if "DefaultWidgetWidth" not in dashboard_config else dashboard_config["DefaultWidgetWidth"] 234 | height = 6 if "DefaultWidgetHeight" not in dashboard_config else dashboard_config["DefaultWidgetHeight"] 235 | 236 | dashboard = aws_cloudwatch.Dashboard( 237 | self, 238 | f'{dashboard_name}-{Names.unique_id(self)}', 239 | dashboard_name=dashboard_name 240 | ) 241 | 242 | for configuration in sps_configurations: 243 | distributed_config_metrics = [] 244 | print(f"Processing: {configuration['ConfigurationName']}") 245 | # Create a list of cloudwatch metric objects, we need to collect 246 | # At least one SPS score to understand which metrics will be created 247 | # Specially considering the configuration might be using AZ and that will 248 | # Increase a lot the type of metrics and change the dimensions as well 249 | sps_scores = sps.fetchSPSScore(configuration) 250 | for score in sps_scores: 251 | cloudwatch_metric_name = configuration['ConfigurationName'] 252 | unit_type = configuration['TargetCapacityUnitType'] 253 | target_capacity = configuration['TargetCapacity'] 254 | metric_name = f"{cloudwatch_metric_name}-{score['Region']}-{unit_type}-{target_capacity}" 255 | dimensions = { 256 | 'Region': f"{score['Region']}", 257 | 'DiversificationName': f"{cloudwatch_metric_name}", 258 | 'UnitType': f"{unit_type}", 259 | 'TargetCapacity': f"{target_capacity}" 260 | } 261 | if 'AvailabilityZoneId' in score: 262 | metric_name = f"{metric_name}-{score['AvailabilityZoneId']}" 263 | dimensions['AvailabilityZoneId'] = score['AvailabilityZoneId'] 264 | 265 | color_hash = f"{score['Region']}{score['AvailabilityZoneId'] if 'AvailabilityZoneId' in score else '' }" 266 | if color_hash in metric_colors: 267 | color = metric_colors[color_hash] 268 | else: 269 | color = COLOR_LIST[colour_counter] 270 | metric_colors[color_hash] = color 271 | colour_counter += 1 272 | 273 | distributed_config_metrics.append(aws_cloudwatch.Metric( 274 | metric_name=metric_name, 275 | namespace=sps.SPS_METRIC_NAMESPACE, 276 | dimensions_map=dimensions, 277 | label=metric_name, 278 | period=aws_cdk.Duration.minutes(15), 279 | statistic='max', 280 | color=color 281 | )) 282 | 283 | graph_widget = aws_cloudwatch.GraphWidget( 284 | title=f"SPS for {configuration['ConfigurationName']}-{unit_type}-{target_capacity}", 285 | left=distributed_config_metrics, 286 | width=width, 287 | height=height, 288 | left_y_axis=aws_cloudwatch.YAxisProps( 289 | label='SPS', 290 | max=10, 291 | min=0, 292 | show_units=False 293 | ) 294 | ) 295 | widgets.append(graph_widget) 296 | 297 | dashboard.add_widgets(*widgets) 298 | dashboards.append(dashboard) 299 | 300 | CfnOutput(self, "SPSLambdaARN", value=sps_lambda.function_arn) 301 | for dashboard in dashboards: 302 | sanitised_dashboard_name = dashboard.dashboard_name.lower().replace(" ", "_")[:max(len(dashboard.dashboard_name), 60)] 303 | CfnOutput( 304 | self, 305 | f'SPSDashboard-{sanitised_dashboard_name}', 306 | value=dashboard.dashboard_arn, 307 | description="Spot Placement Score Dashboard" 308 | ) 309 | 310 | 311 | app = cdk.App() 312 | 313 | if app.node.try_get_context("cdk-nag"): 314 | Aspects.of(app).add(AwsSolutionsChecks(verbose=True)) 315 | 316 | spot_placement_score_dashboard_stack = SpotPlacementScoreDashboardStack( 317 | app, 318 | "spot-placement-score-dashboard", 319 | stack_name=app.node.try_get_context("stack-name"), 320 | description="Guidance for EC2 Spot Placement Score AWS (SO9399)" 321 | ) 322 | app.synth() -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 app.py", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "requirements*.txt", 11 | "source.bat", 12 | "**/__init__.py", 13 | "python/__pycache__", 14 | "venv" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 19 | "@aws-cdk/core:stackRelativeExports": true, 20 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 21 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 22 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 23 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 24 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 25 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 26 | "@aws-cdk/core:checkSecretUsage": true, 27 | "@aws-cdk/aws-iam:minimizePolicies": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 30 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 31 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 32 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 33 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 34 | "@aws-cdk/core:enablePartitionLiterals": true, 35 | "@aws-cdk/core:target-partitions": [ 36 | "aws", 37 | "aws-cn" 38 | ], 39 | "sps-config": "./sps_configuration/sps_config.yaml", 40 | "stack-name": "spot-placement-score-dashboard", 41 | "cdk-nag": false 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/SpotPlacementScore.drawio: -------------------------------------------------------------------------------- 1 | 7VtLc+I4EP4te+A4Lll+H8Mju1ObVKWGw0xOlMACtGNbrCwCzK9fyZbBtkwgAZOwQ5KqWG2pZau/r7v1cMfqxes/GVrMH2mIow4E4bpj9TsQmtC3xD8p2eSSwPZzwYyRUFXaCYbkF1ZCoKRLEuK0UpFTGnGyqAonNEnwhFdkiDG6qlab0qja6wLNsCYYTlCkS7+TkM9zqQ+9nfwvTGbzomfTDfI7MSoqqzdJ5yikq5LIGnSsHqOU51fxuocjOXjFuOTt7vfc3T4Ywwk/psHL4zLyYHz/78Ng0x8PH7qPAf+itLygaKle+O77UAh6EV2G6rn5phiMBSUJzwbU6Yo/0V8PdBxxpydLBnRqgnrZqwpMvSR1VAX1slcVmHX1Zq1/s/6AJYFWqqgHtf5B6QHFn9WlSx6RBPe20ANCOGMoJMIkPRpRJmQJTcTodec8jkTJFJerOeF4uEATOaorQRshm9KEK/CbsCirgZdaBbw5En0xpSOzBGaDF5wbJK8TRWiRkvG2FcOTJUvJC/6G01y5lAogLuR1vJ5JzhpoldrGjNHlInv8r6KvxrsjcTmaSGCMUMSlIs7oT1y8aAda4vdegq87JVFUG4AXzDgRvLqLyEzq51R2h1QpwtNMoxgVksweslLfAmokmroIUTrHoXolhWLRBV7vpYe5JZ3wVpjGmLONqFI0CBRPlaMybVVe7WjvFHXmJco7rhIi5WpmW907NooLRci3kNPX2KlRMv2J+WSuhqERkPsGcA9Qy6YT1bsA+HZfM7aqXDFCYeEHNMbRE00JJxmSxpRzGh+EwARLRFepcogWKF3kLzola/kcGi/MfXjHssaIkxiPxijN2h4Fov1edC+ynCqwAh1Xnq/DqpCdHVX2G0HV6PQbHX+T828MAHoQqFTL3HJDD3Vhk8zThaZerfDkurBJ1hS26q3NhtZmrfX+oHEsR8W9e88fALt0r0+Em1dUSyiTmKqTuA+cnuk1eexp9vMZmdzIWoZTumQTnMcoEd/SpmgVoXgcohYZDV2/GivgB3Pa0jidecCuSKlFegvdLFyPmbiaySuRdSZZhRvzr4n59/a9330b87s903Lc34b5WVQf56i/WED/cPZ7Gvuz6dt3JKl8Y/jlGH4bzs/lMC3b9uHNYb7iMLPp/Eo6ihFsNWGyDNOpeE3L+WCvqU+uHzfDp2FfWHdMEQv1tEn8R7G0XjJO80UbJp+fsp8RRaI+uLtNz880PS/hUlqbTNIRSVKJirRNmFrVyG67OkZtz7AaYGq3BdNiIbyM02xEOjLt0DJ7mkyLRd0bDE+GIYoQi9sEHLQrgNsCsAQ42IA22Bra9A2Bk71i948bHq/aLVo2NKzg83lGfdWj1/9bAm45EfjSkboi4lEheEbCUjXQNntSMpNBPU3xgYWSFhBs3fsutP9/CB5nthlJU4zo+B+hpU3k2rVNnYaZuuMYnqej1mkNtfr6exHPG4P5bcvnaoJ5bVX444O5vjD0KtSSG9SuBGqWbdbn0x+PtkBDm4YnnIR38lCONEiE0lRAsWIRRpdJuDW5GCG2+VEuPMuC4RTF/rp8s78pl54wI+LFshMTuyMCONSO+xxjCAg4YjP8Wj272WAMR4iTl2qnTWOvmj5JiJVOJcDaTpMTVFXkizyqVfn0T00R1BT5hlNVlb+ipioDw/aN3o8PqE9kB2s8WXKc6v4IC87I4ROPCGKR4upIEobMXEG+tkV+od2xlxKgNPrXvURMwjDabRaoY2RKWWebMLyLwsGJkDiJjlCfyZ1KxzXhP9QdeZ2TETqquCOjLGxKhTNTMYf8ISoeQdl8AnF2zlq2e4hqx7LW9g+qapu1UIPRg5jWC0KC0ppAw0pUPssCxTTq+ui777zYheirT25fz85ue0+fa+/pdkzn4N4TjRci+rc5/fd8w69NyoCjpckX3XaCTltxWSXGKjIXUfoKwzJoNuqpYdmpxVL33WHZAcAAwHUsFwAPBtCtTsU81/DEHV/IHehB4F02Yh9xavgciZ93vYkfbCnx+10Qps/0756+ymQPRZHMDDnV88FBTzYZPsmvS4aYvRBhyWvMCv2PzAotfQ59Kre3ayy7ZZXn8r0LrrEc5HexF36Q4F4r/K4fD7D82pcfx7Lb9GwjENqAyEYC13RqfsM2Bb1NLwh8F7oBhIF7WXq/cebRwrowHEDv7nrWhffv1QbHZrcHthYCwwnKP9VjA5Zna0mt7RoNaa0F9oP+JM/kfgLQyOrXA5o3bPqTDCZn+cqtBhzX0oDTOBtyW4KNdYSzeVtAOyIGHRtu3COjzakLRfuW/muH0cB7o41dPbvhwpqiluOJpS9lfVvKJX2QHde4vhwwB+3ZDf4F1tKAqgI6nRYHUY42kCjuPjDPq+8+07cG/wE= -------------------------------------------------------------------------------- /docs/SpotPlacementScore.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-ec2-spot-placement-score-tracker/da264d5f27fa88f76c9f282b9e3839e1da4ed716/docs/SpotPlacementScore.drawio.png -------------------------------------------------------------------------------- /docs/building-a-spot-placement-score-tracker-dashboard-on-aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-ec2-spot-placement-score-tracker/da264d5f27fa88f76c9f282b9e3839e1da4ed716/docs/building-a-spot-placement-score-tracker-dashboard-on-aws.png -------------------------------------------------------------------------------- /docs/spot-placement-score.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-ec2-spot-placement-score-tracker/da264d5f27fa88f76c9f282b9e3839e1da4ed716/docs/spot-placement-score.png -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aws-cdk-lib==2.130.0 2 | aws-cdk.aws-lambda-python-alpha==2.130.0a0 3 | constructs>=10.0.0,<11.0.0 4 | boto3>=1.22.11 5 | pyyaml>=5.0 6 | cdk-nag==2.19.1 -------------------------------------------------------------------------------- /source.bat: -------------------------------------------------------------------------------- 1 | ### Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | ### SPDX-License-Identifier: MIT-0 3 | ### 4 | ### Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | ### software and associated documentation files (the "Software"), to deal in the Software 6 | ### without restriction, including without limitation the rights to use, copy, modify, 7 | ### merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | ### permit persons to whom the Software is furnished to do so. 9 | ### 10 | ### THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | ### INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | ### PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | ### HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | ### OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | ### SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | # 17 | # Author: Carlos Manzanedo Rueda 18 | 19 | @echo off 20 | 21 | rem The sole purpose of this script is to make the command 22 | rem 23 | rem source .venv/bin/activate 24 | rem 25 | rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. 26 | rem On Windows, this command just runs this batch file (the argument is ignored). 27 | rem 28 | rem Now we don't need to document a Windows command for activating a virtualenv. 29 | 30 | echo Executing .venv\Scripts\activate.bat for you 31 | .venv\Scripts\activate.bat 32 | -------------------------------------------------------------------------------- /spot_placement_score_lambda/__init__.py: -------------------------------------------------------------------------------- 1 | from .spot_placement_score_lambda import * -------------------------------------------------------------------------------- /spot_placement_score_lambda/requirements.txt: -------------------------------------------------------------------------------- 1 | pyyaml>=5.0 2 | boto3>=1.22.11 3 | -------------------------------------------------------------------------------- /spot_placement_score_lambda/spot_placement_score_lambda.py: -------------------------------------------------------------------------------- 1 | ### Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | ### SPDX-License-Identifier: MIT-0 3 | ### 4 | ### Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | ### software and associated documentation files (the "Software"), to deal in the Software 6 | ### without restriction, including without limitation the rights to use, copy, modify, 7 | ### merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | ### permit persons to whom the Software is furnished to do so. 9 | ### 10 | ### THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | ### INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | ### PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | ### HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | ### OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | ### SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | # 17 | # Author: Carlos Manzanedo Rueda 18 | 19 | import json 20 | import logging 21 | import os 22 | import boto3 23 | import sys 24 | import yaml 25 | 26 | logger = logging.getLogger() 27 | logger.setLevel(logging.INFO) 28 | 29 | handler = logging.StreamHandler(sys.stdout) 30 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 31 | handler.setFormatter(formatter) 32 | logger.addHandler(handler) 33 | 34 | S3_CONFIGURATION_BUCKET_ENV_NAME = "S3_CONFIGURATION_BUCKET" 35 | S3_CONFIGURATION_OBJECT_KEY = "S3_CONFIGURATION_OBJECT_KEY" 36 | SPS_METRIC_NAMESPACE = "Spot Placement Score Metrics" 37 | DEBUG = 'DEBUG' 38 | DEBUG_CONFIG_FILE = 'DEBUG_CONFIG_FILE' 39 | 40 | # Note a single configuration does support either a list of InstanceTypes 41 | # Or an object that defines attribute instance selection InstanceRequirementsWithMetadata 42 | # as defined here: 43 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.get_spot_placement_scores 44 | 45 | 46 | 47 | def loadConfigurations(): 48 | """ 49 | Loads the configuration from an S3 bucket. The configuration is expected to exist 50 | in a file named 'sps_config.yaml' and have a structure similar to the one commented 51 | above in this file. The configuration maps very closely with the Boto3 API for 52 | [Spot placement score](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.get_spot_placement_scores) 53 | :return: a list with the diversified configurations that will be processed by SPS 54 | and added to cloudwatch 55 | """ 56 | if os.getenv(DEBUG) is not None: 57 | # debug mode allows us to load the configuration file from a local file 58 | configuration_path = os.getenv(DEBUG_CONFIG_FILE) 59 | if configuration_path is not None and os.path.isfile(configuration_path): 60 | logger.info(f"Debug mode detected, loading configuration from {configuration_path}") 61 | with open(configuration_path, 'r') as file: 62 | config_yaml = yaml.load(file.read(), Loader=yaml.loader.SafeLoader) 63 | logger.debug("Configuration successfully parsed from yaml to object") 64 | return config_yaml 65 | else: 66 | logger.error(f"Debug mode could not find the path for file {configuration_path}") 67 | 68 | s3_bucket = os.getenv(S3_CONFIGURATION_BUCKET_ENV_NAME) 69 | if s3_bucket is None: 70 | msg = f"Could not find required environment variable {S3_CONFIGURATION_BUCKET_ENV_NAME}" 71 | logger.error(msg) 72 | raise Exception(msg) 73 | 74 | s3_object = os.getenv(S3_CONFIGURATION_OBJECT_KEY) 75 | if s3_object is None: 76 | msg = f"Could not find required environment variable {S3_CONFIGURATION_OBJECT_KEY}" 77 | logger.error(msg) 78 | raise Exception(msg) 79 | 80 | logger.info(f"About to fetch configuration from bucket {s3_bucket}") 81 | s3_client = boto3.client('s3') 82 | response = s3_client.get_object( 83 | Bucket=s3_bucket, 84 | Key=s3_object 85 | ) 86 | 87 | status_code = response['ResponseMetadata']['HTTPStatusCode'] 88 | logger.info(f"Response Code: {status_code}") 89 | if status_code != 200: 90 | msg = f"Could not retrieve the s3://{s3_bucket}/{S3_CONFIGURATION_FILE_NAME}" 91 | logger.error(msg) 92 | raise Exception(msg) 93 | 94 | config_doc = response['Body'].read() 95 | config_yaml = yaml.load(config_doc, Loader=yaml.loader.SafeLoader) 96 | logger.debug("Configuration successfully parsed from yaml to object") 97 | return config_yaml 98 | 99 | 100 | def __validateConfiguration(configuration=None): 101 | # For the moment I'll leave a pretty dumb validation function, 102 | # But in the future this should hold a schema that verifies the configuration 103 | # Section using schema libraries such as [Cerberus](https://docs.python-cerberus.org/en/stable/) 104 | if configuration is None: 105 | msg = f"Configuration does not exist or is malformed" 106 | logger.error(msg) 107 | return msg 108 | 109 | missing_fields = [] 110 | for key in ['ConfigurationName', 'TargetCapacity', 'TargetCapacityUnitType', 111 | 'SingleAvailabilityZone', 'RegionNames']: 112 | if key not in configuration: 113 | missing_fields.append(key) 114 | 115 | if 'InstanceTypes' not in configuration and 'InstanceRequirementsWithMetadata' not in configuration: 116 | missing_fields.append('InstanceTypes or InstanceRequirementsWithMetadata') 117 | 118 | if not missing_fields: 119 | return None 120 | else: 121 | return f"The configuration is missing a few fields. Fields missing : {missing_fields}" 122 | 123 | 124 | def fetchSPSScore(configuration=None): 125 | ec2_client = boto3.client('ec2') 126 | response = None 127 | if 'InstanceTypes' in configuration: 128 | response = ec2_client.get_spot_placement_scores( 129 | TargetCapacity=configuration['TargetCapacity'], 130 | InstanceTypes=configuration['InstanceTypes'], 131 | TargetCapacityUnitType=configuration['TargetCapacityUnitType'], 132 | SingleAvailabilityZone=configuration['SingleAvailabilityZone'], 133 | RegionNames=configuration['RegionNames'] 134 | ) 135 | else: 136 | response = ec2_client.get_spot_placement_scores( 137 | TargetCapacity=configuration['TargetCapacity'], 138 | InstanceRequirementsWithMetadata=configuration['InstanceRequirementsWithMetadata'], 139 | TargetCapacityUnitType=configuration['TargetCapacityUnitType'], 140 | SingleAvailabilityZone=configuration['SingleAvailabilityZone'], 141 | RegionNames=configuration['RegionNames'] 142 | ) 143 | status_code = response['ResponseMetadata']['HTTPStatusCode'] 144 | logger.info(f"Response Code: {status_code}") 145 | if status_code != 200: 146 | logger.error("Could not retrieve the Spot Placement Score") 147 | 148 | spot_placement_scores = response['SpotPlacementScores'] 149 | logger.debug(f"Got response : {response}") 150 | logger.info(f"Spot Placement Score: {spot_placement_scores}") 151 | return spot_placement_scores 152 | 153 | 154 | def __putSPSMetricsInCloudwatch(configuration, spot_placement_scores=None): 155 | if spot_placement_scores is None: 156 | msg = "Spot Placement Scores was None, cannot insert in cloudwatch" 157 | raise Exception(msg) 158 | 159 | cloudwatch_client = boto3.client('cloudwatch') 160 | cloudwatch_metric_name = configuration['ConfigurationName'] 161 | target_capacity = configuration['TargetCapacity'] 162 | unit_type = configuration['TargetCapacityUnitType'] 163 | 164 | # if 'AvailabilityZoneId' not in score else f"{cloudwatch_metric_name}-{score['Region']}-{unit_type}-{target_capacity}-{score['AvailabilityZoneId']}" 165 | 166 | metric_data = [ 167 | { 168 | 'MetricName': f"{cloudwatch_metric_name}-{score['Region']}-{unit_type}-{target_capacity}" 169 | if 'AvailabilityZoneId' not in score else 170 | f"{cloudwatch_metric_name}-{score['Region']}-{unit_type}-{target_capacity}-{score['AvailabilityZoneId']}", 171 | 'Dimensions': [ 172 | { 173 | 'Name': 'Region', 174 | 'Value': f"{score['Region']}" 175 | }, 176 | { 177 | 'Name': 'DiversificationName', 178 | 'Value': f"{cloudwatch_metric_name}" 179 | }, 180 | { 181 | 'Name': 'UnitType', 182 | 'Value': f"{unit_type}" 183 | }, 184 | { 185 | 'Name': 'TargetCapacity', 186 | 'Value': f"{target_capacity}" 187 | } 188 | ] if 'AvailabilityZoneId' not in score else [ 189 | { 190 | 'Name': 'Region', 191 | 'Value': f"{score['Region']}" 192 | }, 193 | { 194 | 'Name': 'DiversificationName', 195 | 'Value': f"{cloudwatch_metric_name}" 196 | }, 197 | { 198 | 'Name': 'UnitType', 199 | 'Value': f"{unit_type}" 200 | }, 201 | { 202 | 'Name': 'TargetCapacity', 203 | 'Value': f"{target_capacity}" 204 | }, 205 | { 206 | 'Name': 'AvailabilityZoneId', 207 | 'Value': score['AvailabilityZoneId'] 208 | } 209 | ] 210 | , 211 | 'Unit': 'Count', 212 | 'Value': score['Score']} 213 | for score in spot_placement_scores 214 | ] 215 | logger.debug(f"Metric data value: {metric_data}") 216 | 217 | response = cloudwatch_client.put_metric_data( 218 | MetricData=metric_data, 219 | Namespace=SPS_METRIC_NAMESPACE 220 | ) 221 | 222 | logger.debug(f"Got response {response}") 223 | status_code = response['ResponseMetadata']['HTTPStatusCode'] 224 | if status_code != 200: 225 | logger.error("Could not store metrics to cloudwatch") 226 | logger.info(f"Data stored to cloudwatch") 227 | return metric_data 228 | 229 | def handler(event, context): 230 | # Retrieve configuration from S3 (get environment variable) 231 | dashboard_config = loadConfigurations() 232 | 233 | # removes potential duplicates in metrics and SPS queries 234 | configurations = [ 235 | json.loads(config) 236 | for config in 237 | list({json.dumps(config, sort_keys=True, indent=0) for config in list([config for sublist in 238 | list([configuration['Sps'] for configuration in dashboard_config]) 239 | for config in sublist])}) 240 | ] 241 | 242 | # TODO: Change this simple validation for cerberus schema evaluation 243 | validation_errors = list( 244 | filter(lambda x: x is not None, 245 | [__validateConfiguration(config) for config in configurations])) 246 | if validation_errors: 247 | msg = f'Got errors when validating internal elements in configuration : {str(validation_errors)}' 248 | logger.error(msg) 249 | raise Exception(msg) 250 | 251 | # For each of the entries in the configuration 252 | metric_data_results = [] 253 | for configuration in configurations: 254 | try: 255 | logger.info(f"About to load scores for {configuration['ConfigurationName']}") 256 | spot_placement_scores = fetchSPSScore(configuration) 257 | logger.info(f"About to put scores for {configuration['ConfigurationName']} into cloudwatch") 258 | metric_data = __putSPSMetricsInCloudwatch(configuration, spot_placement_scores) 259 | metric_data_results.append(metric_data) 260 | except Exception as e: 261 | logger.error(f" Error while processing {configuration['ConfigurationName']}: {e}") 262 | # We shallow the exception and attempt to continue with the loop 263 | # raise(e) 264 | 265 | return { 266 | "statusCode": 200, 267 | "body": json.dumps({ 268 | "result": metric_data_results 269 | }), 270 | } 271 | 272 | 273 | if __name__ == "__main__": 274 | handler(None, None) 275 | -------------------------------------------------------------------------------- /sps_configuration/sps_config.yaml: -------------------------------------------------------------------------------- 1 | ### Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | ### SPDX-License-Identifier: MIT-0 3 | ### 4 | ### Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | ### software and associated documentation files (the "Software"), to deal in the Software 6 | ### without restriction, including without limitation the rights to use, copy, modify, 7 | ### merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | ### permit persons to whom the Software is furnished to do so. 9 | ### 10 | ### THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | ### INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | ### PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | ### HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | ### OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | ### SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | # 17 | ### This example showcases how to create actionable insights for Spot configurations 18 | ### The final goal is find configurations for your workload that and increase 19 | ### diversification and as a result increase the Spot Placement Score. 20 | ### This dashboard will be created with 4 columns and 2 rows. 21 | ### The rows at the top will show what the SPS score is for a 22 | ### Request of 250 vCPU's and the second row for a 125 vCPUs 23 | ### The numbers have been kept deliberately low so this work in most accounts 24 | ### as an example. You are encouraged to modify this to your needs. 25 | ### 26 | ### The first column `R 8xl and Above` is the 27 | ### base configuration that represents the current configuration used by the application 28 | ### The rest of the columns contain proposed changes in the configuration to increase the diversification 29 | ### either by adding instance types from different sizes or by 30 | ### adding different families. Once that you tried this configuration, 31 | ### as an exercise: Increase diversification with new families. 32 | - Dashboard: R-historic-SPS-example 33 | DefaultWidgetHeight: 12 # Default : 6 34 | DefaultWidgetWidth: 6 # Default : 6, Max:24 35 | Sps: 36 | ##################################################### 37 | ## R Configurations for 2.5K vCPUs in existing regions 38 | ##################################################### 39 | - ConfigurationName: R 8xl and Above 40 | InstanceTypes: 41 | - r3.8xlarge 42 | - r4.8xlarge 43 | - r5.8xlarge 44 | - r5.8xlarge 45 | - r5d.8xlarge 46 | - r5dn.8xlarge 47 | - r5n.8xlarge 48 | - r6i.8xlarge 49 | - r5.12xlarge 50 | - r5n.12xlarge 51 | - r6i.12xlarge 52 | - r4.16xlarge 53 | - r5a.16xlarge 54 | - r5d.16xlarge 55 | - r5ad.16xlarge 56 | RegionNames: 57 | - us-east-1 58 | - eu-west-1 59 | - eu-north-1 60 | - ap-southeast-1 61 | SingleAvailabilityZone: False 62 | TargetCapacity: 250 63 | TargetCapacityUnitType: vcpu 64 | - ConfigurationName: R 4xl and Above 65 | InstanceTypes: 66 | - r3.8xlarge 67 | - r4.8xlarge 68 | - r5.8xlarge 69 | - r5.8xlarge 70 | - r5d.8xlarge 71 | - r5dn.8xlarge 72 | - r5n.8xlarge 73 | - r6i.8xlarge 74 | - r5.12xlarge 75 | - r5n.12xlarge 76 | - r6i.12xlarge 77 | - r4.16xlarge 78 | - r5a.16xlarge 79 | - r5d.16xlarge 80 | - r5ad.16xlarge 81 | - r5.4xlarge 82 | - r5a.4xlarge 83 | - r5ad.4xlarge 84 | - r5b.4xlarge 85 | - r5d.4xlarge 86 | - r5dn.4xlarge 87 | - r5n.4xlarge 88 | - r6a.4xlarge 89 | - r6i.4xlarge 90 | - r6id.4xlarge 91 | RegionNames: 92 | - us-east-1 93 | - eu-west-1 94 | - eu-north-1 95 | - ap-southeast-1 96 | SingleAvailabilityZone: False 97 | TargetCapacity: 250 98 | TargetCapacityUnitType: vcpu 99 | - ConfigurationName: ABIS-1to8GB-Above32vCPUs 100 | InstanceRequirementsWithMetadata: 101 | ArchitectureTypes: 102 | - x86_64 103 | InstanceRequirements: 104 | VCpuCount: 105 | Min: 32 106 | MemoryMiB: 107 | Min: 256 108 | AcceleratorCount: 109 | Max: 0 110 | BareMetal: excluded 111 | BurstablePerformance: excluded 112 | CpuManufacturers: 113 | - intel 114 | - amd 115 | InstanceGenerations: 116 | - current 117 | MemoryGiBPerVCpu: 118 | Min: 8 119 | SpotMaxPricePercentageOverLowestPrice: 50 120 | RegionNames: 121 | - us-east-1 122 | - eu-west-1 123 | - eu-north-1 124 | - ap-southeast-1 125 | SingleAvailabilityZone: False 126 | TargetCapacity: 250 127 | TargetCapacityUnitType: vcpu 128 | - ConfigurationName: ABIS-1to8GB-Above16vCPUs 129 | InstanceRequirementsWithMetadata: 130 | ArchitectureTypes: 131 | - x86_64 132 | InstanceRequirements: 133 | VCpuCount: 134 | Min: 16 135 | MemoryMiB: 136 | Min: 128 137 | AcceleratorCount: 138 | Max: 0 139 | BareMetal: excluded 140 | BurstablePerformance: excluded 141 | CpuManufacturers: 142 | - intel 143 | - amd 144 | InstanceGenerations: 145 | - current 146 | MemoryGiBPerVCpu: 147 | Min: 8 148 | SpotMaxPricePercentageOverLowestPrice: 50 149 | RegionNames: 150 | - us-east-1 151 | - eu-west-1 152 | - eu-north-1 153 | - ap-southeast-1 154 | SingleAvailabilityZone: False 155 | TargetCapacity: 250 156 | TargetCapacityUnitType: vcpu 157 | ##################################################### 158 | # R configurations for 1.5K vCPUs in existing regions 159 | ##################################################### 160 | - ConfigurationName: R 8xl and Above 161 | InstanceTypes: 162 | - r3.8xlarge 163 | - r4.8xlarge 164 | - r5.8xlarge 165 | - r5.8xlarge 166 | - r5d.8xlarge 167 | - r5dn.8xlarge 168 | - r5n.8xlarge 169 | - r6i.8xlarge 170 | - r5.12xlarge 171 | - r5n.12xlarge 172 | - r6i.12xlarge 173 | - r4.16xlarge 174 | - r5a.16xlarge 175 | - r5d.16xlarge 176 | - r5ad.16xlarge 177 | RegionNames: 178 | - us-east-1 179 | - eu-west-1 180 | - eu-north-1 181 | - ap-southeast-1 182 | SingleAvailabilityZone: False 183 | TargetCapacity: 125 184 | TargetCapacityUnitType: vcpu 185 | - ConfigurationName: R 4xl and Above 186 | InstanceTypes: 187 | - r3.8xlarge 188 | - r4.8xlarge 189 | - r5.8xlarge 190 | - r5.8xlarge 191 | - r5d.8xlarge 192 | - r5dn.8xlarge 193 | - r5n.8xlarge 194 | - r6i.8xlarge 195 | - r5.12xlarge 196 | - r5n.12xlarge 197 | - r6i.12xlarge 198 | - r4.16xlarge 199 | - r5a.16xlarge 200 | - r5d.16xlarge 201 | - r5ad.16xlarge 202 | - r5.4xlarge 203 | - r5a.4xlarge 204 | - r5ad.4xlarge 205 | - r5b.4xlarge 206 | - r5d.4xlarge 207 | - r5dn.4xlarge 208 | - r5n.4xlarge 209 | - r6a.4xlarge 210 | - r6i.4xlarge 211 | - r6id.4xlarge 212 | RegionNames: 213 | - us-east-1 214 | - eu-west-1 215 | - eu-north-1 216 | - ap-southeast-1 217 | SingleAvailabilityZone: False 218 | TargetCapacity: 125 219 | TargetCapacityUnitType: vcpu 220 | - ConfigurationName: ABIS-1to8GB-Above32vCPUs 221 | InstanceRequirementsWithMetadata: 222 | ArchitectureTypes: 223 | - x86_64 224 | InstanceRequirements: 225 | VCpuCount: 226 | Min: 32 227 | MemoryMiB: 228 | Min: 256 229 | AcceleratorCount: 230 | Max: 0 231 | BareMetal: excluded 232 | BurstablePerformance: excluded 233 | CpuManufacturers: 234 | - intel 235 | - amd 236 | InstanceGenerations: 237 | - current 238 | MemoryGiBPerVCpu: 239 | Min: 8 240 | SpotMaxPricePercentageOverLowestPrice: 50 241 | RegionNames: 242 | - us-east-1 243 | - eu-west-1 244 | - eu-north-1 245 | - ap-southeast-1 246 | SingleAvailabilityZone: False 247 | TargetCapacity: 125 248 | TargetCapacityUnitType: vcpu 249 | - ConfigurationName: ABIS-1to8GB-Above16vCPUs 250 | InstanceRequirementsWithMetadata: 251 | ArchitectureTypes: 252 | - x86_64 253 | InstanceRequirements: 254 | VCpuCount: 255 | Min: 16 256 | MemoryMiB: 257 | Min: 128 258 | AcceleratorCount: 259 | Max: 0 260 | BareMetal: excluded 261 | BurstablePerformance: excluded 262 | CpuManufacturers: 263 | - intel 264 | - amd 265 | InstanceGenerations: 266 | - current 267 | MemoryGiBPerVCpu: 268 | Min: 8 269 | SpotMaxPricePercentageOverLowestPrice: 50 270 | RegionNames: 271 | - us-east-1 272 | - eu-west-1 273 | - eu-north-1 274 | - ap-southeast-1 275 | SingleAvailabilityZone: False 276 | TargetCapacity: 125 277 | TargetCapacityUnitType: vcpu --------------------------------------------------------------------------------