Blog DevOps

FinOps : downscale clusters ECS et bases RDS | Theodo Cloud

Rédigé par Louis Viennot | 24 oct. 2024 12:00:00

AWS Elastic Container Service est un service d’orchestration de conteneurs 100% managé par Amazon. Il permet d’exécuter et de scaler facilement des charges de travail. Cet article s'adresse particulièrement aux infrastructures qui utilisent ECS pour héberger des applications conteneurisées et RDS stocker leurs données.

Exemple d’infrastructure compatible

Une optimisation FinOps et Green IT à fort impact ?

Dans de nombreuses entreprises, les environnements cloud hors production ne sont utilisés que pendant les heures de bureau pour le développement, les tests et l'assurance qualité. En dehors de ces périodes, ces environnements restent actifs, consommant des ressources inutilement. Sur une journée, cela représente entre 10 et 14 heures où les clusters ECS et les bases RDS sont inactifs — soit plus de la moitié du temps.

En incluant les week-ends, pendant lesquels ces environnements sont rarement utilisés, les ressources provisionnées sont potentiellement inutilisées pendant 60% du temps. Cette situation offre une excellente opportunité d'optimisation FinOps : en éteignant automatiquement ces services hors des heures de travail, il est possible de réaliser des économies considérables sans affecter la productivité des équipes.

Au-delà de l'aspect financier, cette approche a également un impact environnemental positif. En réduisant le temps d'utilisation des ressources cloud, on diminue la consommation d'énergie des data centers. Cette démarche s'inscrit dans une logique de Green IT, contribuant à réduire l'empreinte carbone des infrastructures numériques. Ainsi, l'optimisation FinOps devient non seulement un levier économique, mais aussi un geste écologique responsable.

Par exemple, des clients de Theodo Cloud ont réussi à réduire significativement leurs coûts en appliquant cette stratégie. Les économies réalisées varient de 10 % à 17 % sur la facture finale comprenant tous les environnements, selon les cas. Cette approche, simple à mettre en œuvre, offre un retour sur investissement rapide sans la complexité d'autres optimisations techniques.

Downsizer les tâches ECS avec une AutoScaling ScheduledAction

Pour optimiser les coûts liés à ECS, nous allons utiliser une Autoscaling ScheduledAction pour ajuster automatiquement le nombre de tâches en fonction des heures de travail. Les autoscaling scheduled actions sont des tâches automatisées permettant de mettre à jour les paramètres de son cluster ECS à un moment précis, par exemple pour anticiper un pic de charge.

Le paramétrage proposé dans la suite est sous forme de code Terraform, mais il est possible de le faire via la CLI AWS. En revanche, la gestion de l’autoscaling via des actions planifiées n’est pas disponible dans la console web AWS. Notons enfin que l’autoscaling doit être activé sur vos services. Voici comment procéder :

Mettre en place la structure du module


Il faut commencer par créer un module terraform afin de déployer les ressources nécessaires. Il est possible de les intégrer au module qui gère vos services ECS, ou bien d’en créer un nouveau. Afin de s’adapter au plus de cas possible, nous allons choisir la seconde solution ici.

Il faut donc créer un nouveau module, nous pouvons utiliser la structure suivante :

ecs-shutdown/
├── main.tf
├── variables.tf
├── outputs.tf
└── README.md

Cette structure de base comprend les fichiers essentiels pour un module Terraform :

  • main.tf : Contient la logique principale et les ressources à créer
  • variables.tf : Définit les variables d'entrée du module

Définir les ScheduledActions dans notre module


Ensuite, il va falloir créer deux app autoscaling scheduled actions : une pour réduire le nombre de tâches en dehors des heures de travail, et une autre pour les augmenter au début de la journée :

resource "aws_appautoscaling_scheduled_action" "stop" {
  for_each = var.services
  name               = "stop_${each.key}"
  service_namespace  = "ecs"
  resource_id        = each.value.resource_id
  scalable_dimension = "ecs:service:DesiredCount"
  schedule           = each.value.stop_schedule != null ? each.value.stop_schedule : var.stop_schedule

  scalable_target_action {
    min_capacity = 0
    max_capacity = 0
  }

  depends_on = [aws_appautoscaling_scheduled_action.start]
}

resource "aws_appautoscaling_scheduled_action" "start" {
  name               = "start_${each.key}"
  service_namespace  = "ecs"
  resource_id        = each.value.resource_id
  scalable_dimension = "ecs:service:DesiredCount"
  schedule           = each.value.shutdown_schedule != null ? each.value.stop_schedule : var.stop_schedule

  scalable_target_action {
    max_capacity = each.value.original_max_capacity
    min_capacity = each.value.original_min_capacity
  }
}

Appeler le module Terraform


Pour utiliser ce module, nous devons définir les variables nécessaires dans le fichier variables.tf. Voici un exemple de contenu pour ce fichier :

variable "services" {
  type = map(object({
    resource_id = string
    original_max_capacity = number
    original_min_capacity = number
    stop_schedule = string
    start_schedule = string
  }))
  description = "Map of ECS services to manage"
}

variable "stop_schedule" {
  type = string
  default = "cron(0 20 ? * MON-FRI *)"
  description = "Default cron schedule for stopping services"
}

variable "start_schedule" {
  type = string
  default = "cron(0 8 ? * MON-FRI *)"
  description = "Default cron schedule for starting services"
}

Ensuite, nous pouvons appeler notre module dans un fichier Terragrunt :

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "${get_repo_root()}/modules//ecs-shutdown"
}

inputs = {
  services = {
    "api-service" = {
      resource_id = "service/my-cluster/api-service"
      original_max_capacity = 5
      original_min_capacity = 1
      stop_schedule = "cron(0 19 ? * MON-FRI *)"
      start_schedule = "cron(0 7 ? * MON-FRI *)"
    },
    "worker-service" = {
      resource_id = "service/my-cluster/worker-service"
      original_max_capacity = 3
      original_min_capacity = 1
      stop_schedule  = "cron(0 2 ? * TUE-SAT)" 
      start_schedule = "cron(0 7 ? * MON-FRI)"
    }
  }
}

Cet exemple montre comment configurer deux services ECS avec des horaires d'arrêt et de démarrage personnalisés. Notons la subtilité au niveau des jours de démarrage lorsque la tâche doit terminer après minuit pour permettre des traitements nocturnes.

Éteindre et rallumer une base RDS avec une MaintenanceWindow

Côté bases de données, nous allons utiliser une MaintenanceWindow pour éteindre et rallumer automatiquement notre base RDS en fonction des heures de travail.

Les maintenance windows sont des tâches automatisées permettant de programmer et de lancer diverses tâches de maintenances : par exemple des mises à jour d’instances EC2, des évolutions de configuration de bases de données…

Nous pouvons exploiter l’automatisation liée à cette fonctionnalité pour éteindre et rallumer une RDS. Comme précédemment, le paramétrage proposé est sous forme de code Terraform, mais il est possible de le faire via la CLI AWS. Voici comment procéder :

Mettre en place la structure du module


Comme précédemment, nous allons créer un nouveau module afin de simplifier l’intégration dans toute codebase IaC.

rds-shutdown/
├── iam.tf
├── maintenance.tf
├── variables.tf
├── outputs.tf
└── README.md

Définir les permissions nécessaires


Il faut dans un premier temps donner accès à un rôle IAM à notre maintenance window afin qu’elle puisse s’exécuter correctement et agir sur notre base. Ces accès sont liés au fonctionnement interne d’AWS, et donnent à la tâche les accès aux ressources dont elle a besoin pour s’exécuter, ainsi que les accès pour arrêter et démarrer les bases de données RDS.

data "aws_iam_policy_document" "start_stop_rds" {
  statement {
	  actions = [
      "rds:StartDBCluster",
      "rds:StopDBCluster",
      "rds:ListTagsForResource",
      "rds:DescribeDBInstances",
      "rds:StopDBInstance",
      "rds:DescribeDBClusters",
      "rds:StartDBInstance"
    ]
    resources = [ var.rds_arn ]
  }
  statement {
    actions = [
      "states:DescribeExecution",
      "states:StartExecution"
    ]
    resources = [
      "arn:aws:states:*:*:execution:*:*",
      "arn:aws:states:*:*:stateMachine:*"
    ]
  }
  statement {
    actions = [
      "lambda:InvokeFunction"
    ]
    resources = ["arn:aws:lambda:*:*:function:*"]
  }
  statement {
    actions = [
      "iam:PassRole"
    ]
    resources = [
      "*"
    ]
    condition {
      test     = "StringEquals"
      variable = "iam:PassedToService"
      values = [
        "ssm.amazonaws.com"
      ]
    }
  }
  statement {
    actions = [
      "resource-groups:ListGroupResources",
      "resource-groups:ListGroups",
      "resource-groups:GetGroupQuery",
      "resource-groups:GetTags",
      "resource-groups:GetGroup",
      "resource-groups:SearchResources",
      "ssm:SendCommand",
      "ssm:CancelCommand",
      "ssm:ListCommands",
      "ssm:ListCommandInvocations",
      "ssm:GetCommandInvocation",
      "ssm:GetAutomationExecution",
      "ssm:StartAutomationExecution",
      "ssm:ListTagsForResource",
      "ssm:GetParameters",
      "tag:GetResources"
    ]
    resources = ["*"]
  }
  
}

resource "aws_iam_policy" "maintenance" {
  name        = "StartStopRDS"
  path        = "/"
  description = "The policy to start and stop any rds"

  policy = data.aws_iam_policy_document.start_stop_rds.json
}

resource "aws_iam_role" "maintenance" {
  name                = "rds_ssm_maintenance_window"
  managed_policy_arns = [aws_iam_policy.maintenance.arn]
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ssm.amazonaws.com"
        }
      },
    ]
  })
}

Définir notre maintenance window, nos cibles et la tâche à effectuer


Dans un premier temps, on doit définir un resource group qui cible notre base de données :

resource "aws_resourcegroups_group" "this" {
  name = "rds-ssm-resource-group"

  resource_query {
    query = jsonencode({
      ResourceTypeFilters = [
        "AWS::RDS::DBInstance"
      ]
      TagFilters = [
        {
          Key = "Name"
          Values = [
            var.identifier
          ]
        }
      ]
    })
  }
}

Ensuite, nous pouvons créer la maintenance window avec le schedule souhaité :

resource "aws_ssm_maintenance_window" "start" {
  name              = "start-rds"
  schedule_timezone = "Europe/Paris"
  schedule          = var.start_schedule
  duration          = 1
  cutoff            = 0
}

Nous allons maintenant définir la cible de cette maintenance (notre base RDS), ainsi que l’action à effectuer : AWS-StartRdsInstance. Cette action est définie par AWS

resource "aws_ssm_maintenance_window_target" "start" {
  window_id     = aws_ssm_maintenance_window.start.id
  name          = "start-rds"
  resource_type = "RESOURCE_GROUP"

  targets {
    key    = "resource-groups:Name"
    values = [aws_resourcegroups_group.this.name]
  }
}

resource "aws_ssm_maintenance_window_task" "start" {
  task_arn         = "AWS-StartRdsInstance" # AWS defined
  name             = "start-instance"
  task_type        = "AUTOMATION"
  window_id        = aws_ssm_maintenance_window.start.id
  service_role_arn = aws_iam_role.maintenance.arn
  max_concurrency  = 1
  max_errors       = 5

  targets {
    key    = "WindowTargetIds"
    values = [aws_ssm_maintenance_window_target.start.id]
  }

  task_invocation_parameters {
    automation_parameters {
      document_version = "$LATEST"

      parameter {
        name   = "InstanceId"
        values = [""]
      }

      parameter {
        name   = "AutomationAssumeRole"
        values = [aws_iam_role.maintenance.arn]
      }
    }
  }
}

Il suffit ensuite de répliquer ces ressources pour la tâche d’arrêt des bases de données :

# #############
# STOP TASK #
# #############


resource "aws_ssm_maintenance_window" "stop" {
  count = var.start_stop_enabled ? 1 : 0

  name              = "stop-rds"
  schedule_timezone = "Europe/Paris"
  schedule          = var.stop_schedule
  duration          = 1
  cutoff            = 0
}

resource "aws_ssm_maintenance_window_target" "stop" {
  count = var.start_stop_enabled ? 1 : 0

  window_id     = aws_ssm_maintenance_window.stop[0].id
  name          = "stop-rds"
  resource_type = "RESOURCE_GROUP"

  targets {
    key    = "resource-groups:Name"
    values = [aws_resourcegroups_group.this[0].name]
  }
}

resource "aws_ssm_maintenance_window_task" "stop" {
  count = var.start_stop_enabled ? 1 : 0

  task_arn         = "AWS-StopRdsInstance"
  name             = "stop-instance"
  task_type        = "AUTOMATION"
  window_id        = aws_ssm_maintenance_window.stop[0].id
  service_role_arn = aws_iam_role.maintenance[0].arn
  max_concurrency  = 1
  max_errors       = 5

  targets {
    key    = "WindowTargetIds"
    values = [aws_ssm_maintenance_window_target.stop[0].id]
  }

  task_invocation_parameters {
    automation_parameters {
      document_version = "$LATEST"

      parameter {
        name   = "InstanceId"
        values = [""]
      }

      parameter {
        name   = "AutomationAssumeRole"
        values = [aws_iam_role.maintenance[0].arn]
      }
    }
  }
}

Appeler le module terraform


Pour utiliser notre module terraform, nous pouvons l'appeler dans un fichier Terragrunt comme suit :

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "${get_repo_root()}/modules//rds-shutdown"
}

inputs = {
  identifier = "my-rds-instance"
  rds_arn = "arn:aws:rds:eu-west-1:123456789012:db:my-rds-instance"
  start_schedule = "cron(0 7 ? * MON-FRI *)"
  stop_schedule = "cron(0 19 ? * MON-FRI *)"
  start_stop_enabled = true
}

Ce code configure notre module pour démarrer l'instance RDS à 7h00 et l'arrêter à 19h00 du lundi au vendredi.

Conclusion

En conclusion, cet article vous a présenté des outils natifs d'AWS pour optimiser vos coûts d'infrastructure et réduire votre empreinte carbone. Ces solutions, plus faciles à maintenir et transparentes que des fonctions Lambda, offrent une gestion automatisée des ressources ECS et RDS.

L'équipe d'infogérance de Theodo Cloud a déjà implémenté ces pratiques FinOps et Green IT chez plusieurs clients, démontrant un retour sur investissement rapide et significatif. Ainsi, l’adoption de ces méthodes, permet non seulement de réduire l’impact économique de votre infrastructure, mais aussi de contribuer à une utilisation plus responsable des ressources cloud.