Todos ouvimos que recursos em nuvem são ilimitados, que podemos usar o quanto quisermos, que o céu é o limite, que na nuvem nada se acaba e outras coisas. Mas tudo isto tem limite sim, o “bolso” da empresa.

Eu vi e vejo muito algumas empresas contratando o EKS (Kubernetes da AWS) e na sequência subindo instâncias EC2 com máquinas pesadas, e depois RDS’s como se não houvesse amanhã. A nuvem nos proporciona isso, tudo é muito fácil e atrativo. O problema é a conta no fim do mês.

Uma das formas de economizarmos e reduzir os custos da infraestrutura é simplesmente desligá-la quando não precisamos usar.

Ambientes como desenvolvimento e homologação (ou qualquer outra que não seja o de produção) pode ser desligado em determinados períodos e com isto reduzimos o billing no final do mês.

Neste artigo vou mostrar como desligar as instâncias do RDS fora do horário de expediente e nos finais de semana. Com isto já conseguimos reduzir bastante no custo com os recursos de infraestrutura na nuvem.

Iniciando o projeto

Não vamos abordar os detalhes de como se cria um um projeto e como se configura os arquivos do Terraform. Para início, vamos criar o arquivo main.tf e vamos declarar o provider da AWS que vamos utilizar.

provider "aws" {
  region  = "us-east-1"
}

Vamos configurar o arquivo vars.tf com as seguintes variáveis que vamos utilizar no nosso projeto:

variable "policy_name" { 
  default = "ScheduleRDSPolicy"
}

variable "policy_description" { 
  default = "Policy que permite o Lambda a desligar e ligar as instâncias do RDS"
}

variable "role_name" { 
  default = "ScheduleRDSRole"
}

variable "role_description" { 
  default = "Role que permite o Lambda a desligar e ligar as instâncias do RDS"
}

variable "lambda_function_name" { 
  default = "ScheduleRDSLambda"
}

variable "lambda_handler" { 
  default = "lambda_function.lambda_handler"
}

variable "lambda_timeout" { 
  default = "10"
}

variable "lambda_dbinstances" { 
  default = "eventstore-db,kong-db"
}

variable "cloudwatch_name" { 
  default = "ScheduleRDSCloudwatch"
}

variable "cloudwatch_description" { 
  default = "Liga e desliga os recursos do RDS"
}

A organização do projeto deve ficar da seguinte forma:

Criando a Policy

No arquivo main.tf, vamos criar a Policy para dar permissão de Start e Stop no RDS para o Lambda, além de dar as permissões de salvar os logs no CloudWatch.

resource "aws_iam_policy" "schedule_rds_policy" {
  name          = "${var.policy_name}"
  description   = "${var.policy_description}"
  policy        = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {           
            "Effect": "Allow",
            "Action": [
                "rds:Stop*",
                "rds:Start*"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}
EOF
}

Criando a Role

Em seguida criaremos a Role para dar permissão ao Lambda de executar os comandos no RDS.

resource "aws_iam_role" "schedule_rds_role" {
  name                  = "${var.role_name}"
  description           = "${var.role_description}"
  assume_role_policy    = <<EOF
{
    "Version": "2012-10-17",
    "Statement": 
    [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "Service": "lambda.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
EOF
}

Na sequência vamos anexar a Role na Policy.

resource "aws_iam_role_policy_attachment" "schedule_iam_attachment" {
  role          = "${aws_iam_role.schedule_rds_role.name}"
  policy_arn    = "${aws_iam_policy.schedule_rds_policy.arn}"
}

Criando a função Python

Vamos configurar o Lambda para executar uma função em Python. Esta função recebe como parâmetro os nomes dos bancos de dados RDS.

A função está configurada para manter as instâncias de RDS ligadas durante o horaŕio de expediente, ou seja, das 8h às 19h. Fora do expediente e aos finais de semana ele fica desligado.

Caso seja necessário outros períodos é só ajustarmos as variáveis da função.

import boto3
import datetime
import calendar
import os
from datetime import date, datetime, timedelta

rds = boto3.client('rds')

def lambda_handler(event, context):

    dateNow = date.today()
    dayOfWeek = calendar.day_name[dateNow.weekday()]
    dbinstances = os.environ['dbinstances'].split(',')

    time = datetime.now()
    now = (time - timedelta(hours = 3))
    hour = now.hour

    print('Iniciando o lambda de schedule RDS em ' + str(dayOfWeek) + ' às ' + str(hour))

    if len(dbinstances) == 0:
        print('Nenhuma instância RDS foi cadastrada.')
        return

    if dayOfWeek == 'Saturday' or dayOfWeek == 'Sunday':
        print('Final de semana.')
        return

    if hour > 7 and hour < 19:
        print('Iniciando as instâncias de RDS')
        
        for instance in dbinstances:
            rds.start_db_instance(DBInstanceIdentifier = instance)
    else:
        print('Parando as instâncias de RDS')
        
        for instance in dbinstances:
            rds.stop_db_instance(DBInstanceIdentifier = instance)
        
    print('Finalizando o lambda de schedule RDS.')

Criando a função Lambda

Agora vamos criar o resource Lambda que executará a função lambda_handler do arquivo lambda_function.py.

resource "aws_lambda_function" "schedule_rds_lambda" {
  filename          = "${data.archive_file.file_lambda.output_path}"
  function_name     = "${var.lambda_function_name}"
  role              = "${aws_iam_role.schedule_rds_role.arn}"
  handler           = "${var.lambda_handler}"
  timeout           = "${var.lambda_timeout}"
  source_code_hash  = "${data.archive_file.file_lambda.output_base64sha256}"
  runtime           = "python3.8"

  environment {
    variables = {
      dbinstances = "${var.lambda_dbinstances}"
    }
  }
}

data "archive_file" "file_lambda" {
  type = "zip"
  source_file = "lambda_function.py"
  output_path = "lambda_function.zip"
}

No resource schedule_rds_lambda temos a variável dbinstances. Nesta variável colocaremos os nomes das instâncias RDS que queremos fazer o agendamento de Start/Stop.

Junto com a criação do Lambda criarmos também uma função data. Esta função é responsável por pegar o arquivo Python, fazer o zip e prepará-lo para o upload no Lambda.

Criando o CloudWatch

Por último criamos o resource para o CloudWatch. Neste resource configuramos o CloudWatch para ser executado a cada 60 minutos e em seguida anexamos o CloudWatch ao Lambda que criamos anteriormente.

resource "aws_cloudwatch_event_rule" "schedule_cloudwatch" {
  name                  = "${var.cloudwatch_name}"
  description           = "${var.cloudwatch_description}"
  schedule_expression   = "rate(60 minutes)"
}

resource "aws_cloudwatch_event_target" "schedule_cloudwatch_target" {
  rule      = "${aws_cloudwatch_event_rule.schedule_cloudwatch.name}"
  target_id = "lambdaRDS"
  arn       = "${aws_lambda_function.schedule_rds_lambda.arn}"
}

Executando o projeto

Com tudo configurado agora podemos executar os comandos para o Terraform escrever nossa infra as a code na AWS.

terraform init
terraform plan
terraform apply

Resultado

Agora vamos conferir para ver se realmente o Terraform criou tudo o que precisamos para agendar o Start/Stop das nossas instâncias RDS.

Policy

Role

Lambda

CloudWatch

Conclusão

Eu fiz isto aqui na empresa onde trabalho e o resultado foi bastante considerável. Desligar os recursos quando não estão sendo utilizados trouxe uma boa economia e este processo pode ser aplicado em todos os ambientes, exceto produção, claro.

Este mesmo projeto também pode ser usado para desligar instâncias do EC2, por exemplo. Basta alterar as funções do arquivo Python para desligar o EC2 ou qualquer outro recurso que queira.

Os fontes deste artigo podem ser encontrados no meu GitHub.