From 1e619a9106946b41d6e151a333065269c5894d18 Mon Sep 17 00:00:00 2001 From: Wessel van Heerde Date: Mon, 23 Dec 2024 11:47:29 +0100 Subject: [PATCH] enhancement: Improve docs and validations (#8) * feat: improve TF validations * docs: add setup instructions to README * docs(readme): update module usage * docs: add development instructions * docs: add emojis * docs: fix consistency * revert: remove KMS key validation * docs: fix consistency --------- Co-authored-by: github-actions[bot] --- README.md | 50 ++++++++++++++++++++++++++++------ examples/office_hours/main.tf | 2 +- examples/on_demand/main.tf | 2 +- examples/webhooks/main.tf | 2 +- scheduler/scheduler/schemas.py | 8 +++--- variables.tf | 42 +++++++++++++++++++++++++++- 6 files changed, 89 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 3c8214e..c876737 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ It has the following high level architecture: ![Architecture](https://raw.githubusercontent.com/schubergphilis/terraform-aws-mcaf-resource-scheduler/main/docs/architecture.png) -## Features +## :books: Features ### Supported resource types @@ -52,7 +52,27 @@ Optionally a pair of webhooks can be deployed to trigger starting or stopping an Webhooks require an API key and can be setup to only allow certain IP addresses. A POST request has to be made to one of the outputted endpoints to trigger a webhook. -## Limitations +## :wrench: Setup + +To setup this module requires a composition of the resources that need to be managed. Based on that input, a state machine is generated. Be aware that the composition dictates order: the resources in the composition are controlled in that order when the composition is started. The order is reversed when the composition is stopped. + +Please see the [examples](examples/) folder for code examples of how to implement this module. + +### Configuration + +Resource types require certain parameters in order to function. It's recommended to fill the parameters by refering to existing resources in your TF code. + +| Resource | Resource Type | Required Parameters | +| --- | --- | --- | +| EC2 Auto-Scaling Group | auto_scaling_group | **name:** the name of the auto-scaling group to control
**min:** the minimal number of instances to run (used on start of composition)
**max:** the maximum number of instances to run (used on start of composition)
**desired:** the desired number of instances to run (used on start of composition) | +| EC2 Instance | ec2_instance | **id:** the ID of the instance to control | +| ECS Service | ecs_service | **cluster_name:** the name of the ECS cluster the task runs on
**desired:** the desired number of tasks (used on start of composition)
**name:** the name of the ECS task to control | +| FSX Windows Filesystem | fsx_windows_file_system | **id:** the ID of the filesystem to control
**throughput_capacity:** the throughput capacity of the filesystem (used on start of composition) | +| RDS Cluster | rds_cluster | **id:** the ID of the cluster to control | +| RDS Instance | rds_instance | **id:** the ID of the instance to control | +| Redshift Cluster | redshift_cluster | **id:** the ID of the cluster to control | + +## :rotating_light: Limitations ### Schedule mixing @@ -78,17 +98,29 @@ Throughput can't be changed until 6 hours after the last change was requested. A See https://docs.aws.amazon.com/fsx/latest/WindowsGuide/managing-storage-configuration.html#managing-storage-capacity for more information. -## Setup +## :test_tube: Development -To setup this module requires a composition of the resources that need to be managed. Based on that input, a state machine is generated. +This module uses the integrated Lambda to abstract some of the more complex functionality away. For redistribution purposes, the following dependencies have been vendorized: -Please see the [examples](examples/) folder for code examples of how to implement this module. +* pyawscron 1.0.6: https://pypi.org/project/pyawscron/ - https://github.com/pitchblack408/pyawscron/tree/1.0.6 -## Development +### Adding support for more resources -This module uses the integrated Lambda to abstract some of the more complex functionality away. For redistribution purposes, the following dependencies have been vendorized: +This module is extendable. To add support for more resources, follow these general steps: -* pyawscron 1.0.6: https://pypi.org/project/pyawscron/ - https://github.com/pitchblack408/pyawscron/tree/1.0.6 +1. In the Lambda code + 1. Add a test + 1. Add the resource controller + 1. Add the resource to the handler + 1. Add the resource to the schema + 1. Make sure tests pass + +1. In the Terraform code + 1. Add the resource to the resource_composition variable + 1. Add validation to the resource_composition variable + 1. Add an example + 1. Update this README + 1. Make sure validations pass ## Requirements @@ -182,7 +214,7 @@ This module uses the integrated Lambda to abstract some of the more complex func | [stop\_resources\_at](#input\_stop\_resources\_at) | Resources stop cron expression in selected timezone | `string` | n/a | yes | | [tags](#input\_tags) | Mapping of tags | `map(string)` | `{}` | no | | [timezone](#input\_timezone) | Timezone to execute schedules in | `string` | `"UTC"` | no | -| [webhooks](#input\_webhooks) | Deploy webhooks for external triggers |
object({
deploy = bool
ip_whitelist = list(string)
private = optional(bool, false)
})
|
{
"deploy": false,
"ip_whitelist": [],
"private": false
}
| no | +| [webhooks](#input\_webhooks) | Deploy webhooks for external triggers from whitelisted IP CIDR's. |
object({
deploy = bool
ip_whitelist = list(string)
private = optional(bool, false)
})
|
{
"deploy": false,
"ip_whitelist": [],
"private": false
}
| no | ## Outputs diff --git a/examples/office_hours/main.tf b/examples/office_hours/main.tf index 7141e62..f252417 100644 --- a/examples/office_hours/main.tf +++ b/examples/office_hours/main.tf @@ -21,7 +21,7 @@ module "scheduler" { { "type" : "ecs_service", "params" : { - "cluster_name" : "application-cluster-1" + "cluster_name" : "application-cluster-1", "name" : "application-service-1", "desired" : 2 } diff --git a/examples/on_demand/main.tf b/examples/on_demand/main.tf index 1ad8636..3a08dcc 100644 --- a/examples/on_demand/main.tf +++ b/examples/on_demand/main.tf @@ -28,7 +28,7 @@ module "scheduler" { { "type" : "ecs_service", "params" : { - "cluster_name" : "application-cluster-1" + "cluster_name" : "application-cluster-1", "name" : "application-service-1", "desired" : 2 } diff --git a/examples/webhooks/main.tf b/examples/webhooks/main.tf index c5e0913..f56bc6d 100644 --- a/examples/webhooks/main.tf +++ b/examples/webhooks/main.tf @@ -21,7 +21,7 @@ module "scheduler" { { "type" : "ecs_service", "params" : { - "cluster_name" : "application-cluster-1" + "cluster_name" : "application-cluster-1", "name" : "application-service-1", "desired" : 2 } diff --git a/scheduler/scheduler/schemas.py b/scheduler/scheduler/schemas.py index 344f05b..05be203 100644 --- a/scheduler/scheduler/schemas.py +++ b/scheduler/scheduler/schemas.py @@ -55,10 +55,10 @@ "auto_scaling_group_params": { "type": "object", "properties": { - "name": {"type": "string"}, - "min": {"type": "string"}, - "max": {"type": "string"}, "desired": {"type": "string"}, + "max": {"type": "string"}, + "min": {"type": "string"}, + "name": {"type": "string"}, }, "required": ["name", "min", "max", "desired"], }, @@ -68,8 +68,8 @@ "cluster_name": { "type": "string", }, - "name": {"type": "string"}, "desired": {"type": "string"}, + "name": {"type": "string"}, }, "required": ["cluster_name", "name", "desired"], }, diff --git a/variables.tf b/variables.tf index 134b86e..6be1836 100644 --- a/variables.tf +++ b/variables.tf @@ -14,6 +14,46 @@ variable "resource_composition" { condition = length([for r in var.resource_composition : r if contains(["ec2_instance", "rds_instance", "rds_cluster", "auto_scaling_group", "ecs_service", "redshift_cluster", "wait", "fsx_windows_file_system"], r.type)]) == length(var.resource_composition) error_message = "Resource type must be one of ec2_instance, rds_instance, rds_cluster, auto_scaling_group, ecs_service, redshift_cluster or fsx_windows_file_system" } + + validation { + condition = !contains([for r in var.resource_composition : (r.type == "auto_scaling_group" ? keys(r.params) == tolist(["desired", "max", "min", "name"]) : true)], false) + error_message = "Auto-scaling group resources must have 'desired', 'max', 'min' and 'name' parameters" + } + + validation { + condition = !contains([for r in var.resource_composition : (r.type == "ec2_instance" ? keys(r.params) == tolist(["id"]) : true)], false) + error_message = "EC2 instance resources must have 'id' parameter" + } + + validation { + condition = !contains([for r in var.resource_composition : (r.type == "ecs_service" ? keys(r.params) == tolist(["cluster_name", "desired", "name"]) : true)], false) + error_message = "ECS Service resources must have 'cluster_name', 'desired' and 'name' parameters" + } + + validation { + condition = !contains([for r in var.resource_composition : (r.type == "fsx_windows_file_system" ? keys(r.params) == tolist(["id", "throughput_capacity"]) : true)], false) + error_message = "FSx Windows Filesystem resources must have 'id' and 'throughput_capacity' parameters" + } + + validation { + condition = !contains([for r in var.resource_composition : (r.type == "rds_cluster" ? keys(r.params) == tolist(["id"]) : true)], false) + error_message = "RDS Cluster resources must have 'id' parameter" + } + + validation { + condition = !contains([for r in var.resource_composition : (r.type == "rds_instance" ? keys(r.params) == tolist(["id"]) : true)], false) + error_message = "RDS Instance resources must have 'id' parameter" + } + + validation { + condition = !contains([for r in var.resource_composition : (r.type == "redshift_cluster" ? keys(r.params) == tolist(["id"]) : true)], false) + error_message = "Redshift Cluster resources must have 'id' parameter" + } + + validation { + condition = !contains([for r in var.resource_composition : (r.type == "wait" ? keys(r.params) == tolist(["seconds"]) : true)], false) + error_message = "Wait instructions must have 'seconds' parameter" + } } variable "webhooks" { @@ -27,7 +67,7 @@ variable "webhooks" { ip_whitelist = [] private = false } - description = "Deploy webhooks for external triggers" + description = "Deploy webhooks for external triggers from whitelisted IP CIDR's." } variable "composition_name" {